Skip to content

Commit 1f5497b

Browse files
authored
Merge pull request #1907 from SamXop123/leaderboard-migration
Leaderboard migration
2 parents 3d609f9 + 2bac08e commit 1f5497b

3 files changed

Lines changed: 33 additions & 427 deletions

File tree

docusaurus.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,9 @@ const config: Config = {
281281
],
282282
],
283283

284-
// ✅ Add this customFields object to expose the token to the client-side
284+
// Custom fields exposed to client-side code (browser-visible at build time)
285285
customFields: {
286+
backendApiUrl: process.env.BACKEND_API_URL || "http://localhost:5000",
286287
gitToken: process.env.DOCUSAURUS_GIT_TOKEN,
287288
clerkPublishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY || "",
288289
// Shopify credentials for merch store

src/lib/statsProvider.tsx

Lines changed: 31 additions & 194 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ export function CommunityStatsProvider({
163163
const {
164164
siteConfig: { customFields },
165165
} = useDocusaurusContext();
166-
const token = customFields?.gitToken || "";
166+
const backendApiUrl = (customFields?.backendApiUrl as string) || "http://localhost:5000";
167167

168168
const [loading, setLoading] = useState(false); // Start with false to avoid hourglass
169169
const [error, setError] = useState<string | null>(null);
@@ -251,164 +251,6 @@ export function CommunityStatsProvider({
251251
setCurrentTimeFilter(filter);
252252
}, []);
253253

254-
const fetchAllOrgRepos = useCallback(
255-
async (headers: Record<string, string>) => {
256-
const repos: any[] = [];
257-
let page = 1;
258-
while (true) {
259-
const resp = await fetch(
260-
`https://api.github.com/orgs/${GITHUB_ORG}/repos?type=public&per_page=100&page=${page}`,
261-
{
262-
headers,
263-
},
264-
);
265-
if (!resp.ok) {
266-
throw new Error(
267-
`Failed to fetch org repos: ${resp.status} ${resp.statusText}`,
268-
);
269-
}
270-
const data = await resp.json();
271-
repos.push(...data);
272-
if (!Array.isArray(data) || data.length < 100) break;
273-
page++;
274-
}
275-
return repos;
276-
},
277-
[],
278-
);
279-
280-
const fetchMergedPRsForRepo = useCallback(
281-
async (repoName: string, headers: Record<string, string>) => {
282-
const mergedPRs: PullRequestItem[] = [];
283-
284-
// First, get the first page to estimate total pages
285-
const firstResp = await fetch(
286-
`https://api.github.com/repos/${GITHUB_ORG}/${repoName}/pulls?state=closed&per_page=100&page=1`,
287-
{ headers },
288-
);
289-
290-
if (!firstResp.ok) {
291-
console.warn(
292-
`Failed to fetch PRs for ${repoName}: ${firstResp.status} ${firstResp.statusText}`,
293-
);
294-
return [];
295-
}
296-
297-
const firstPRs: PullRequestItem[] = await firstResp.json();
298-
if (!Array.isArray(firstPRs) || firstPRs.length === 0) return [];
299-
300-
const firstPageMerged = firstPRs.filter((pr) => Boolean(pr.merged_at));
301-
mergedPRs.push(...firstPageMerged);
302-
303-
// If we got less than 100, that's all there is
304-
if (firstPRs.length < 100) return mergedPRs;
305-
306-
// Create parallel requests for remaining pages
307-
const pagePromises: Promise<PullRequestItem[]>[] = [];
308-
const maxPages = Math.min(MAX_PAGES_PER_REPO, 10);
309-
310-
for (let i = 2; i <= maxPages; i++) {
311-
pagePromises.push(
312-
fetch(
313-
`https://api.github.com/repos/${GITHUB_ORG}/${repoName}/pulls?state=closed&per_page=100&page=${i}`,
314-
{ headers },
315-
)
316-
.then(async (resp) => {
317-
if (!resp.ok) return [];
318-
const prs: PullRequestItem[] = await resp.json();
319-
if (!Array.isArray(prs)) return [];
320-
return prs.filter((pr) => Boolean(pr.merged_at));
321-
})
322-
.catch(() => []),
323-
);
324-
}
325-
326-
// Wait for all pages in parallel
327-
const remainingPages = await Promise.all(pagePromises);
328-
remainingPages.forEach((pagePRs) => {
329-
if (pagePRs.length > 0) mergedPRs.push(...pagePRs);
330-
});
331-
332-
return mergedPRs;
333-
},
334-
[],
335-
);
336-
337-
// Enhanced processing function that stores only valid PRs with points
338-
const processBatch = useCallback(
339-
async (
340-
repos: any[],
341-
headers: Record<string, string>,
342-
): Promise<{
343-
contributorMap: Map<string, FullContributor>;
344-
totalMergedPRs: number;
345-
}> => {
346-
const contributorMap = new Map<string, FullContributor>();
347-
let totalMergedPRs = 0;
348-
349-
// Process repos in batches to control concurrency
350-
for (let i = 0; i < repos.length; i += MAX_CONCURRENT_REQUESTS) {
351-
const batch = repos.slice(i, i + MAX_CONCURRENT_REQUESTS);
352-
353-
const promises = batch.map(async (repo) => {
354-
if (repo.archived) return { mergedPRs: [], repoName: repo.name };
355-
356-
try {
357-
const mergedPRs = await fetchMergedPRsForRepo(repo.name, headers);
358-
return { mergedPRs, repoName: repo.name };
359-
} catch (error) {
360-
console.warn(`Skipping repo ${repo.name} due to error:`, error);
361-
return { mergedPRs: [], repoName: repo.name };
362-
}
363-
});
364-
365-
// Wait for current batch to complete
366-
const results = await Promise.all(promises);
367-
368-
// Process results from this batch
369-
results.forEach(({ mergedPRs, repoName }) => {
370-
mergedPRs.forEach((pr) => {
371-
// Calculate points for this PR based on labels
372-
const prPoints = calculatePointsForPR(pr.labels);
373-
374-
// ONLY store PRs that have points (i.e., have "recode" label and a level label)
375-
if (prPoints > 0) {
376-
totalMergedPRs++;
377-
378-
const username = pr.user.login;
379-
if (!contributorMap.has(username)) {
380-
contributorMap.set(username, {
381-
username,
382-
avatar: pr.user.avatar_url,
383-
profile: pr.user.html_url,
384-
points: 0, // Will be calculated later based on filter
385-
prs: 0, // Will be calculated later based on filter
386-
allPRDetails: [], // Store only valid PRs here
387-
});
388-
}
389-
const contributor = contributorMap.get(username)!;
390-
391-
// Add detailed PR information only if it has all required fields
392-
if (pr.title && pr.html_url && pr.merged_at && pr.number) {
393-
contributor.allPRDetails.push({
394-
title: pr.title,
395-
url: pr.html_url,
396-
mergedAt: pr.merged_at,
397-
repoName,
398-
number: pr.number,
399-
points: prPoints,
400-
});
401-
}
402-
}
403-
});
404-
});
405-
}
406-
407-
return { contributorMap, totalMergedPRs };
408-
},
409-
[fetchMergedPRsForRepo],
410-
);
411-
412254
const fetchAllStats = useCallback(
413255
async (signal: AbortSignal) => {
414256
// Check cache first and load it immediately without showing loading state
@@ -433,57 +275,52 @@ export function CommunityStatsProvider({
433275

434276
setError(null);
435277

436-
if (!token) {
437-
setError(
438-
"GitHub token not found. Please set customFields.gitToken in docusaurus.config.js.",
439-
);
440-
setLoading(false);
441-
return;
442-
}
443-
444278
try {
445-
const headers: Record<string, string> = {
446-
Authorization: `token ${token}`,
447-
Accept: "application/vnd.github.v3+json",
448-
};
449-
450-
// Fetch both org stats and repos in parallel
451-
const [orgStats, repos] = await Promise.all([
452-
githubService.fetchOrganizationStats(signal),
453-
fetchAllOrgRepos(headers),
279+
const [leaderboardResp, statsResp] = await Promise.all([
280+
fetch(`${backendApiUrl}/api/leaderboard`, { signal }),
281+
fetch(`${backendApiUrl}/api/stats`, { signal })
454282
]);
455283

456-
// Set org stats immediately
457-
setGithubStarCount(orgStats.totalStars);
458-
setGithubContributorsCount(orgStats.totalContributors);
459-
setGithubForksCount(orgStats.totalForks);
460-
setGithubReposCount(orgStats.publicRepositories);
461-
setGithubDiscussionsCount(orgStats.discussionsCount);
462-
setLastUpdated(new Date(orgStats.lastUpdated));
463-
464-
// Process leaderboard data with concurrent processing
465-
const { contributorMap, totalMergedPRs } = await processBatch(
466-
repos,
467-
headers,
468-
);
284+
if (!leaderboardResp.ok || !statsResp.ok) {
285+
throw new Error("Failed to fetch leaderboard data from backend server");
286+
}
469287

470-
const contributorsArray = Array.from(contributorMap.values());
288+
const leaderboardData = await leaderboardResp.json();
289+
const statsData = await statsResp.json();
290+
291+
// Set org stats immediately
292+
setGithubStarCount(statsData.totalStars);
293+
setGithubContributorsCount(statsData.totalContributors);
294+
setGithubForksCount(statsData.totalForks);
295+
setGithubReposCount(statsData.publicRepositories);
296+
setGithubDiscussionsCount(statsData.discussionsCount);
297+
setLastUpdated(new Date(statsData.lastUpdated));
298+
299+
// Format to FullContributor (which matches contributor mapping)
300+
const contributorsArray: FullContributor[] = (leaderboardData.contributors || []).map((c: any) => ({
301+
username: c.username,
302+
avatar: c.avatar,
303+
profile: c.profile,
304+
points: c.points,
305+
prs: c.prs,
306+
allPRDetails: c.prDetails || [] // Backend stores complete list
307+
}));
471308

472309
setAllContributors(contributorsArray);
473310

474311
// Cache the results (raw data without filtering)
475312
setCache({
476313
data: {
477314
contributors: contributorsArray,
478-
rawStats: { totalPRs: totalMergedPRs },
315+
rawStats: { totalPRs: statsData.totalContributors },
479316
},
480317
timestamp: now,
481318
});
482319
} catch (err: any) {
483320
if (err.name !== "AbortError") {
484-
console.error("Error fetching GitHub organization stats:", err);
321+
console.error("Error fetching stats from backend:", err);
485322
setError(
486-
err instanceof Error ? err.message : "Failed to fetch GitHub stats",
323+
err instanceof Error ? err.message : "Failed to fetch stats from backend",
487324
);
488325

489326
// Set fallback values on error
@@ -497,7 +334,7 @@ export function CommunityStatsProvider({
497334
setLoading(false);
498335
}
499336
},
500-
[token, fetchAllOrgRepos, processBatch, cache],
337+
[backendApiUrl, cache],
501338
);
502339

503340
const clearCache = useCallback(() => {

0 commit comments

Comments
 (0)