From 80877106140f3cdfe0f725fd2bdf00de0c56c1c1 Mon Sep 17 00:00:00 2001 From: "Juan V." Date: Wed, 3 Sep 2025 21:25:45 +0200 Subject: [PATCH 1/5] made lines of code instead --- scripts/fetch-contributors.js | 200 ++++++++++++- src/data/contributors.json | 82 +++++- src/pages/hall-of-fame.module.css | 303 +++++++++++++++++++- src/pages/hall-of-fame.tsx | 447 ++++++++++++++++++++---------- src/types/contributors.ts | 26 ++ 5 files changed, 890 insertions(+), 168 deletions(-) diff --git a/scripts/fetch-contributors.js b/scripts/fetch-contributors.js index 8478a21..5793eed 100644 --- a/scripts/fetch-contributors.js +++ b/scripts/fetch-contributors.js @@ -7,6 +7,15 @@ const GITHUB_TOKEN = process.env.GITHUB_TOKEN; const ORG_NAME = 'universal-tool-calling-protocol'; const OUTPUT_FILE = path.join(__dirname, '../src/data/contributors.json'); +// Configuration for efficient API batching +const BATCH_CONFIG = { + COMMIT_DETAILS_BATCH_SIZE: 20, // Process commits in batches of 20 + CONCURRENT_REQUESTS: 5, // Max concurrent API requests + RETRY_ATTEMPTS: 3, // Number of retry attempts for failed requests + RATE_LIMIT_DELAY: 100, // Base delay between requests (ms) + COMMIT_ANALYSIS_LIMIT: 1000, // Max commits to analyze (0 = no limit) +}; + const githubApi = async (endpoint) => { const headers = { 'Accept': 'application/vnd.github.v3+json', @@ -50,6 +59,82 @@ const fetchContributorsForRepo = async (repoName) => { } }; +const fetchCommitDetails = async (repoName, commitSha) => { + try { + const commit = await githubApi(`/repos/${ORG_NAME}/${repoName}/commits/${commitSha}`); + return { + additions: commit.stats?.additions || 0, + deletions: commit.stats?.deletions || 0, + total: commit.stats?.total || 0 + }; + } catch (error) { + console.warn(`Could not fetch commit details for ${commitSha}:`, error.message); + return { additions: 0, deletions: 0, total: 0 }; + } +}; + +// Helper function for batched API calls with retry logic +const batchedApiCall = async (items, apiCallFn, batchSize = BATCH_CONFIG.COMMIT_DETAILS_BATCH_SIZE) => { + const results = []; + const batches = []; + + // Split items into batches + for (let i = 0; i < items.length; i += batchSize) { + batches.push(items.slice(i, i + batchSize)); + } + + console.log(` πŸ“¦ Processing ${items.length} items in ${batches.length} batches...`); + + for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) { + const batch = batches[batchIndex]; + console.log(` ⚑ Processing batch ${batchIndex + 1}/${batches.length} (${batch.length} items)...`); + + // Process items in current batch concurrently + const batchPromises = batch.map(async (item) => { + let retryCount = 0; + while (retryCount < BATCH_CONFIG.RETRY_ATTEMPTS) { + try { + const result = await apiCallFn(item); + return result; + } catch (error) { + retryCount++; + if (retryCount === BATCH_CONFIG.RETRY_ATTEMPTS) { + console.warn(` ⚠️ Failed after ${BATCH_CONFIG.RETRY_ATTEMPTS} attempts:`, error.message); + return null; + } + // Exponential backoff for retries + await new Promise(resolve => setTimeout(resolve, BATCH_CONFIG.RATE_LIMIT_DELAY * Math.pow(2, retryCount))); + } + } + }); + + const batchResults = await Promise.all(batchPromises); + results.push(...batchResults.filter(Boolean)); // Filter out null results from failures + + // Rate limiting between batches + if (batchIndex < batches.length - 1) { + await new Promise(resolve => setTimeout(resolve, BATCH_CONFIG.RATE_LIMIT_DELAY)); + } + } + + return results; +}; + +// Helper function to create progress reporting +const createProgressReporter = (total, operation) => { + let processed = 0; + return { + update: () => { + processed++; + const percentage = Math.round((processed / total) * 100); + console.log(` πŸ“Š ${operation}: ${processed}/${total} (${percentage}%)`); + }, + finish: () => { + console.log(` βœ… ${operation}: Completed all ${total} items`); + } + }; +}; + const fetchUserPRsAndActivity = async (username, repoName) => { try { // Get PRs for this user in this repo @@ -60,6 +145,61 @@ const fetchUserPRsAndActivity = async (username, repoName) => { sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); const commits = await githubApi(`/repos/${ORG_NAME}/${repoName}/commits?author=${username}&since=${sixMonthsAgo.toISOString()}&per_page=100`); + // Get ALL commits by this user for line change statistics + let allCommits = []; + let page = 1; + let hasMore = true; + + console.log(` πŸ“Š Fetching all commits for line statistics...`); + while (hasMore && allCommits.length < 500) { // Limit to prevent excessive API calls + try { + const commitPage = await githubApi(`/repos/${ORG_NAME}/${repoName}/commits?author=${username}&per_page=100&page=${page}`); + + if (commitPage.length === 0) { + hasMore = false; + } else { + allCommits = allCommits.concat(commitPage); + page++; + + // Rate limiting for commit fetching + await new Promise(resolve => setTimeout(resolve, 100)); + } + } catch (error) { + console.warn(` Could not fetch commits page ${page} for ${username}:`, error.message); + hasMore = false; + } + } + + // Analyze ALL commits with efficient batching (improved from 100 commit limit) + let totalAdditions = 0; + let totalDeletions = 0; + let totalChanges = 0; + + // Apply configurable limit if set (0 = no limit) + const commitsToAnalyze = BATCH_CONFIG.COMMIT_ANALYSIS_LIMIT > 0 + ? allCommits.slice(0, BATCH_CONFIG.COMMIT_ANALYSIS_LIMIT) + : allCommits; + + console.log(` πŸ“ˆ Analyzing ${commitsToAnalyze.length} commits for line changes (improved batching)...`); + + if (commitsToAnalyze.length > 0) { + // Use efficient batching instead of sequential processing + const commitDetails = await batchedApiCall( + commitsToAnalyze, + async (commit) => await fetchCommitDetails(repoName, commit.sha), + BATCH_CONFIG.COMMIT_DETAILS_BATCH_SIZE + ); + + // Aggregate the results + for (const details of commitDetails) { + totalAdditions += details.additions; + totalDeletions += details.deletions; + totalChanges += details.total; + } + + console.log(` βœ… Successfully analyzed ${commitDetails.length}/${commitsToAnalyze.length} commits`); + } + // Get PR reviews by this user const reviews = await githubApi(`/repos/${ORG_NAME}/${repoName}/pulls/comments?per_page=100`); const userReviews = reviews.filter(review => review.user.login === username); @@ -68,8 +208,15 @@ const fetchUserPRsAndActivity = async (username, repoName) => { prs: prs.length, mergedPrs: prs.filter(pr => pr.merged_at).length, recentCommits: commits.length, + totalCommits: allCommits.length, reviews: userReviews.length, - lastActivity: commits.length > 0 ? commits[0].commit.author.date : null + lastActivity: commits.length > 0 ? commits[0].commit.author.date : null, + // Enhanced line change statistics (now analyzes ALL commits up to limit) + totalAdditions, + totalDeletions, + totalChanges, + commitsAnalyzed: commitsToAnalyze.length, // Total commits we attempted to analyze + commitsSuccessfullyAnalyzed: totalChanges > 0 ? commitsToAnalyze.filter(commit => commit).length : 0 // Successful analyses }; } catch (error) { console.warn(`Could not fetch detailed activity for ${username} in ${repoName}:`, error.message); @@ -77,8 +224,14 @@ const fetchUserPRsAndActivity = async (username, repoName) => { prs: 0, mergedPrs: 0, recentCommits: 0, + totalCommits: 0, reviews: 0, - lastActivity: null + lastActivity: null, + totalAdditions: 0, + totalDeletions: 0, + totalChanges: 0, + commitsAnalyzed: 0, + commitsSuccessfullyAnalyzed: 0 }; } }; @@ -107,7 +260,13 @@ const aggregateContributors = (contributorsByRepo) => { existing.totalPrs += contributor.activityData.prs; existing.totalMergedPrs += contributor.activityData.mergedPrs; existing.totalRecentCommits += contributor.activityData.recentCommits; + existing.totalCommits += contributor.activityData.totalCommits; existing.totalReviews += contributor.activityData.reviews; + // Aggregate line change statistics + existing.totalAdditions += contributor.activityData.totalAdditions; + existing.totalDeletions += contributor.activityData.totalDeletions; + existing.totalChanges += contributor.activityData.totalChanges; + existing.commitsAnalyzed += contributor.activityData.commitsAnalyzed; // Keep the most recent activity date if (contributor.activityData.lastActivity) { const activityDate = new Date(contributor.activityData.lastActivity); @@ -127,8 +286,14 @@ const aggregateContributors = (contributorsByRepo) => { totalPrs: contributor.activityData?.prs || 0, totalMergedPrs: contributor.activityData?.mergedPrs || 0, totalRecentCommits: contributor.activityData?.recentCommits || 0, + totalCommits: contributor.activityData?.totalCommits || 0, totalReviews: contributor.activityData?.reviews || 0, - lastActivity: contributor.activityData?.lastActivity || null + lastActivity: contributor.activityData?.lastActivity || null, + // New line change statistics + totalAdditions: contributor.activityData?.totalAdditions || 0, + totalDeletions: contributor.activityData?.totalDeletions || 0, + totalChanges: contributor.activityData?.totalChanges || 0, + commitsAnalyzed: contributor.activityData?.commitsAnalyzed || 0 }); } }); @@ -194,10 +359,16 @@ const enhanceWithUserData = async (contributors) => { total_prs: contributor.totalPrs, total_merged_prs: contributor.totalMergedPrs, total_recent_commits: contributor.totalRecentCommits, + total_commits: contributor.totalCommits, total_reviews: contributor.totalReviews, last_activity: contributor.lastActivity, pr_success_rate: contributor.totalPrs > 0 ? - Math.round((contributor.totalMergedPrs / contributor.totalPrs) * 100) : 0 + Math.round((contributor.totalMergedPrs / contributor.totalPrs) * 100) : 0, + // New line change statistics + total_additions: contributor.totalAdditions, + total_deletions: contributor.totalDeletions, + total_changes: contributor.totalChanges, + commits_analyzed: contributor.commitsAnalyzed }); // Rate limiting: delay between requests @@ -269,15 +440,19 @@ const calculateAllScores = async (contributorsByRepo, repositories) => { const main = async () => { try { - console.log('πŸš€ Starting contributor data fetch with simplified scoring...'); + console.log('πŸš€ Starting contributor data fetch with enhanced batching and full commit analysis...'); + console.log(`πŸ“Š Configuration: Analyzing up to ${BATCH_CONFIG.COMMIT_ANALYSIS_LIMIT} commits per contributor (0 = no limit)`); + console.log(`⚑ Batching: ${BATCH_CONFIG.COMMIT_DETAILS_BATCH_SIZE} commits per batch with ${BATCH_CONFIG.RETRY_ATTEMPTS} retry attempts`); + console.log('✨ Improvements: Removed 100-commit limit, added efficient batching, retry logic, and better error handling\n'); /* * Process Flow: * 1. Fetch all repositories from the organization * 2. Fetch all contributors from all repositories * 3. Calculate scores for all contributors (simplified recent activity scoring) - * 4. Enhance with detailed user information from GitHub - * 5. Generate and save output file + * 4. Fetch detailed line change statistics for each contributor + * 5. Enhance with detailed user information from GitHub + * 6. Generate and save output file with comprehensive metrics */ // Step 1: Fetch all repositories @@ -308,6 +483,10 @@ const main = async () => { const totalImpactScore = enhancedContributors.reduce((sum, c) => sum + c.impact_score, 0); const totalContributions = enhancedContributors.reduce((sum, c) => sum + c.contributions, 0); const totalRecentActivity = enhancedContributors.reduce((sum, c) => sum + c.total_recent_commits, 0); + const totalAdditions = enhancedContributors.reduce((sum, c) => sum + c.total_additions, 0); + const totalDeletions = enhancedContributors.reduce((sum, c) => sum + c.total_deletions, 0); + const totalChanges = enhancedContributors.reduce((sum, c) => sum + c.total_changes, 0); + const totalCommitsAnalyzed = enhancedContributors.reduce((sum, c) => sum + c.commits_analyzed, 0); // Write to file fs.writeFileSync(OUTPUT_FILE, JSON.stringify({ @@ -317,6 +496,11 @@ const main = async () => { total_impact_score: totalImpactScore, total_recent_activity: totalRecentActivity, scoring_method: 'simplified_recent_activity', + // New aggregated line change statistics + total_additions: totalAdditions, + total_deletions: totalDeletions, + total_changes: totalChanges, + total_commits_analyzed: totalCommitsAnalyzed, contributors: enhancedContributors }, null, 2)); @@ -325,6 +509,8 @@ const main = async () => { console.log(` πŸ’« Total impact score: ${totalImpactScore}`); console.log(` πŸ“ˆ Total contributions: ${totalContributions}`); console.log(` πŸ”₯ Recent activity: ${totalRecentActivity} commits (last 6 months)`); + console.log(` πŸ“Š Line changes: +${totalAdditions.toLocaleString()} -${totalDeletions.toLocaleString()} (${totalChanges.toLocaleString()} total)`); + console.log(` πŸ” Commits analyzed: ${totalCommitsAnalyzed.toLocaleString()}`); console.log(` πŸ† Top contributor: ${enhancedContributors[0]?.name} (${enhancedContributors[0]?.impact_score} impact score)`); } catch (error) { diff --git a/src/data/contributors.json b/src/data/contributors.json index 47b7026..b65ebae 100644 --- a/src/data/contributors.json +++ b/src/data/contributors.json @@ -1,10 +1,14 @@ { - "generated_at": "2025-09-03T14:53:34.638Z", + "generated_at": "2025-09-03T19:07:43.751Z", "total_contributors": 10, "total_contributions": 505, "total_impact_score": 359, "total_recent_activity": 361, "scoring_method": "simplified_recent_activity", + "total_additions": 313932, + "total_deletions": 77961, + "total_changes": 391893, + "total_commits_analyzed": 505, "contributors": [ { "id": 43811028, @@ -35,12 +39,17 @@ ], "repo_count": 9, "impact_score": 169, - "total_prs": 165, + "total_prs": 166, "total_merged_prs": 133, "total_recent_commits": 169, + "total_commits": 169, "total_reviews": 9, "last_activity": "2025-09-02T14:45:10Z", - "pr_success_rate": 81 + "pr_success_rate": 80, + "total_additions": 210552, + "total_deletions": 54504, + "total_changes": 265056, + "commits_analyzed": 169 }, { "id": 170965471, @@ -66,9 +75,14 @@ "total_prs": 86, "total_merged_prs": 64, "total_recent_commits": 100, + "total_commits": 244, "total_reviews": 3, "last_activity": "2025-08-29T12:15:29Z", - "pr_success_rate": 74 + "pr_success_rate": 74, + "total_additions": 39386, + "total_deletions": 11377, + "total_changes": 50763, + "commits_analyzed": 244 }, { "id": 69489757, @@ -94,12 +108,17 @@ ], "repo_count": 4, "impact_score": 38, - "total_prs": 67, + "total_prs": 68, "total_merged_prs": 57, "total_recent_commits": 38, + "total_commits": 38, "total_reviews": 0, "last_activity": "2025-08-21T12:32:23Z", - "pr_success_rate": 85 + "pr_success_rate": 84, + "total_additions": 12605, + "total_deletions": 5194, + "total_changes": 17799, + "commits_analyzed": 38 }, { "id": 5594869, @@ -128,9 +147,14 @@ "total_prs": 54, "total_merged_prs": 44, "total_recent_commits": 24, + "total_commits": 24, "total_reviews": 0, "last_activity": "2025-09-02T14:17:44Z", - "pr_success_rate": 81 + "pr_success_rate": 81, + "total_additions": 12023, + "total_deletions": 3946, + "total_changes": 15969, + "commits_analyzed": 24 }, { "id": 7006124, @@ -156,9 +180,14 @@ "total_prs": 0, "total_merged_prs": 0, "total_recent_commits": 11, + "total_commits": 11, "total_reviews": 0, "last_activity": "2025-08-31T02:56:43Z", - "pr_success_rate": 0 + "pr_success_rate": 0, + "total_additions": 8399, + "total_deletions": 1706, + "total_changes": 10105, + "commits_analyzed": 11 }, { "id": 21357398, @@ -186,9 +215,14 @@ "total_prs": 51, "total_merged_prs": 43, "total_recent_commits": 7, + "total_commits": 7, "total_reviews": 5, "last_activity": "2025-09-02T14:17:22Z", - "pr_success_rate": 84 + "pr_success_rate": 84, + "total_additions": 28358, + "total_deletions": 916, + "total_changes": 29274, + "commits_analyzed": 7 }, { "id": 46451363, @@ -215,9 +249,14 @@ "total_prs": 48, "total_merged_prs": 38, "total_recent_commits": 6, + "total_commits": 6, "total_reviews": 0, "last_activity": "2025-07-31T09:55:48Z", - "pr_success_rate": 79 + "pr_success_rate": 79, + "total_additions": 2282, + "total_deletions": 309, + "total_changes": 2591, + "commits_analyzed": 6 }, { "id": 150209392, @@ -240,12 +279,17 @@ ], "repo_count": 1, "impact_score": 3, - "total_prs": 19, + "total_prs": 20, "total_merged_prs": 19, "total_recent_commits": 4, + "total_commits": 4, "total_reviews": 0, "last_activity": "2025-08-01T15:02:36Z", - "pr_success_rate": 100 + "pr_success_rate": 95, + "total_additions": 7, + "total_deletions": 7, + "total_changes": 14, + "commits_analyzed": 4 }, { "id": 210855846, @@ -271,9 +315,14 @@ "total_prs": 0, "total_merged_prs": 0, "total_recent_commits": 1, + "total_commits": 1, "total_reviews": 0, "last_activity": "2025-07-11T12:42:11Z", - "pr_success_rate": 0 + "pr_success_rate": 0, + "total_additions": 2, + "total_deletions": 2, + "total_changes": 4, + "commits_analyzed": 1 }, { "id": 89096740, @@ -299,9 +348,14 @@ "total_prs": 6, "total_merged_prs": 6, "total_recent_commits": 1, + "total_commits": 1, "total_reviews": 0, "last_activity": "2025-07-24T16:29:37Z", - "pr_success_rate": 100 + "pr_success_rate": 100, + "total_additions": 318, + "total_deletions": 0, + "total_changes": 318, + "commits_analyzed": 1 } ] } \ No newline at end of file diff --git a/src/pages/hall-of-fame.module.css b/src/pages/hall-of-fame.module.css index b931169..f2e79a2 100644 --- a/src/pages/hall-of-fame.module.css +++ b/src/pages/hall-of-fame.module.css @@ -4,6 +4,111 @@ color: #ffffff; } +/* Tooltip styles */ +.tooltipContainer { + position: relative; + display: inline-block; +} + +.tooltipContent { + position: absolute; + bottom: 120%; + left: 50%; + transform: translateX(-50%); + background-color: #333; + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +/* Alternative positioning for tooltips near viewport edges */ +.tooltipContentLeft { + position: absolute; + bottom: 120%; + left: 0; + transform: none; + background-color: #333; + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.tooltipContentRight { + position: absolute; + bottom: 120%; + right: 0; + transform: none; + background-color: #333; + color: white; + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + white-space: nowrap; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.tooltipArrow { + position: absolute; + top: 100%; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #333; +} + +.tooltipArrowLeft { + position: absolute; + top: 100%; + left: 12px; + transform: none; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #333; +} + +.tooltipArrowRight { + position: absolute; + top: 100%; + right: 12px; + transform: none; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 4px solid #333; +} + +/* Avatar styles */ +.avatarImage { + width: 100%; + height: 100%; + border-radius: 8px; + object-fit: cover; +} + +.avatarEmoji { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + font-size: 1.5rem; +} + .header { text-align: center; padding: 4rem 1rem 3rem; @@ -312,12 +417,124 @@ line-height: 1; } -.contributionCount::after { - content: 'contribution score'; +/* Line Changes Display Styles */ +.lineChangesDisplay { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + padding: 0.5rem 1rem; + width: 100%; + text-align: center; +} + +.linesChangedNumber { + font-size: 1.5rem; + font-weight: 700; + color: #ffffff; + line-height: 1; +} + +.linesChangedLabel { font-size: 0.5rem; color: rgba(255, 255, 255, 0.7); text-transform: uppercase; letter-spacing: 0.2px; + font-weight: 500; +} + +/* Enhanced Community Impact Statistics */ +.statsFooter { + margin-top: 3rem; + padding: 3rem 0 2rem; + text-align: center; + background: linear-gradient(135deg, #0a0a0a 0%, #111111 50%, #0a0a0a 100%); + border-radius: 16px; + border: 1px solid #333333; + position: relative; +} + +.statsFooter::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent 0%, #8b5cf6 50%, transparent 100%); + border-radius: 16px 16px 0 0; +} + +.statsFooter h3 { + font-size: 2rem; + font-weight: 700; + margin: 0 0 2rem 0; + background: linear-gradient(135deg, #ffffff 0%, #8b5cf6 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.communityStats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 2rem; + max-width: 800px; + margin: 0 auto; + padding: 0 2rem; +} + +.statItem { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + padding: 1.5rem; + background: linear-gradient(145deg, #1a1a1a 0%, #111111 100%); + border: 1px solid #333333; + border-radius: 12px; + transition: all 0.3s ease; + position: relative; + overflow: hidden; +} + +.statItem::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #8b5cf6 0%, #a78bfa 100%); + opacity: 0; + transition: opacity 0.3s ease; +} + +.statItem:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(139, 92, 246, 0.2); + border-color: #8b5cf6; +} + +.statItem:hover::before { + opacity: 1; +} + +.statNumber { + font-size: 2.5rem; + font-weight: 800; + color: #8b5cf6; + line-height: 1; + text-shadow: 0 2px 4px rgba(139, 92, 246, 0.3); +} + +.statLabel { + font-size: 0.9rem; + color: #cccccc; + text-transform: uppercase; + letter-spacing: 0.5px; + font-weight: 500; + text-align: center; } .callToAction { @@ -495,6 +712,19 @@ font-size: 1.25rem; } + .lineChangesDisplay { + padding: 0.4rem 0.75rem; + gap: 0.2rem; + } + + .linesChangedNumber { + font-size: 1.25rem; + } + + .linesChangedLabel { + font-size: 0.45rem; + } + .ctaButtons { flex-direction: column; align-items: center; @@ -506,6 +736,34 @@ max-width: 300px; justify-content: center; } + + .statsFooter { + margin-top: 2rem; + padding: 2rem 0 1.5rem; + } + + .statsFooter h3 { + font-size: 1.5rem; + margin-bottom: 1.5rem; + } + + .communityStats { + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + padding: 0 1rem; + } + + .statItem { + padding: 1rem; + } + + .statNumber { + font-size: 1.8rem; + } + + .statLabel { + font-size: 0.8rem; + } } @media (max-width: 480px) { @@ -544,4 +802,45 @@ .callToAction { padding: 3rem 1rem; } + + .statsFooter { + margin-top: 1.5rem; + padding: 1.5rem 0 1rem; + } + + .statsFooter h3 { + font-size: 1.25rem; + margin-bottom: 1rem; + } + + .communityStats { + grid-template-columns: 1fr; + gap: 1rem; + padding: 0 1rem; + } + + .statItem { + padding: 1rem 0.75rem; + } + + .statNumber { + font-size: 1.5rem; + } + + .statLabel { + font-size: 0.75rem; + } + + .lineChangesDisplay { + padding: 0.35rem 0.5rem; + gap: 0.15rem; + } + + .linesChangedNumber { + font-size: 1rem; + } + + .linesChangedLabel { + font-size: 0.4rem; + } } \ No newline at end of file diff --git a/src/pages/hall-of-fame.tsx b/src/pages/hall-of-fame.tsx index c04f6d6..35ee8f7 100644 --- a/src/pages/hall-of-fame.tsx +++ b/src/pages/hall-of-fame.tsx @@ -1,50 +1,75 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useMemo, useCallback } from 'react'; import Layout from '@theme/Layout'; import styles from './hall-of-fame.module.css'; import { ContributorsData, GitHubContributor, DisplayContributor } from '../types/contributors'; -// Simple tooltip component +// Constants for better maintainability +const CONTRIBUTOR_STATUS_THRESHOLDS = { + CORE_CONTRIBUTOR: 100, + ACTIVE_CONTRIBUTOR: 50, + REGULAR_CONTRIBUTOR: 20, + INACTIVITY_DAYS: 90, +} as const; + +const SPECIAL_CONTRIBUTORS = { + FOUNDING: ['h3xxit', 'AndreiGS', 'edujuan', 'aliraza1006', 'ulughbeck'] as readonly string[], + ADMINS: ['h3xxit', 'aliraza1006', 'edujuan', 'ulughbeck', 'AndreiGS'] as readonly string[], + LEAD_DEVELOPER: 'Raezil', +} as const; + +// Simple tooltip component with dynamic positioning to prevent edge cutoff const Tooltip: React.FC<{ children: React.ReactNode; text: string }> = ({ children, text }) => { const [isVisible, setIsVisible] = useState(false); + const [position, setPosition] = useState<'center' | 'left' | 'right'>('center'); + const containerRef = useCallback((node: HTMLDivElement | null) => { + if (node && isVisible) { + const rect = node.getBoundingClientRect(); + const tooltipWidth = text.length * 8 + 16; // Approximate tooltip width + const viewportWidth = window.innerWidth; + + // Check if tooltip would overflow on the right + if (rect.left + rect.width / 2 + tooltipWidth / 2 > viewportWidth - 10) { + setPosition('right'); + } + // Check if tooltip would overflow on the left + else if (rect.left + rect.width / 2 - tooltipWidth / 2 < 10) { + setPosition('left'); + } + else { + setPosition('center'); + } + } + }, [isVisible, text]); + + const handleMouseEnter = useCallback(() => setIsVisible(true), []); + const handleMouseLeave = useCallback(() => setIsVisible(false), []); + + // Get the appropriate CSS classes based on position + const getTooltipClasses = () => { + switch (position) { + case 'left': + return { content: styles.tooltipContentLeft, arrow: styles.tooltipArrowLeft }; + case 'right': + return { content: styles.tooltipContentRight, arrow: styles.tooltipArrowRight }; + default: + return { content: styles.tooltipContent, arrow: styles.tooltipArrow }; + } + }; + + const tooltipClasses = getTooltipClasses(); return (
setIsVisible(true)} - onMouseLeave={() => setIsVisible(false)} + ref={containerRef} + className={styles.tooltipContainer} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} > {children} {isVisible && ( -
+
{text} -
+
)}
@@ -61,12 +86,11 @@ const formatDate = (dateString: string | null): string => { // Function to determine contributor status based on contribution score const getContributorStatus = (contributionScore: number, lastActivity: string | null, username: string): string => { // Special status for founding contributors - const foundingContributors = ['h3xxit', 'AndreiGS', 'edujuan', 'aliraza1006', 'ulughbeck']; - if (foundingContributors.includes(username)) { + if (SPECIAL_CONTRIBUTORS.FOUNDING.includes(username)) { return 'Founding Contributor'; } - if (username === 'Raezil') { + if (username === SPECIAL_CONTRIBUTORS.LEAD_DEVELOPER) { return 'Lead Developer'; } @@ -76,13 +100,13 @@ const getContributorStatus = (contributionScore: number, lastActivity: string | (now.getTime() - lastActivityDate.getTime()) / (1000 * 60 * 60 * 24) : Infinity; // Factor in recency for status determination - if (contributionScore >= 100) { - return daysSinceLastActivity <= 90 ? 'Core contributor' : 'Core contributor (inactive)'; + if (contributionScore >= CONTRIBUTOR_STATUS_THRESHOLDS.CORE_CONTRIBUTOR) { + return daysSinceLastActivity <= CONTRIBUTOR_STATUS_THRESHOLDS.INACTIVITY_DAYS ? 'Core contributor' : 'Core contributor (inactive)'; } - if (contributionScore >= 50) { - return daysSinceLastActivity <= 90 ? 'Active contributor' : 'Regular contributor'; + if (contributionScore >= CONTRIBUTOR_STATUS_THRESHOLDS.ACTIVE_CONTRIBUTOR) { + return daysSinceLastActivity <= CONTRIBUTOR_STATUS_THRESHOLDS.INACTIVITY_DAYS ? 'Active contributor' : 'Regular contributor'; } - if (contributionScore >= 20) return 'Regular contributor'; + if (contributionScore >= CONTRIBUTOR_STATUS_THRESHOLDS.REGULAR_CONTRIBUTOR) return 'Regular contributor'; return 'New contributor'; }; @@ -95,16 +119,15 @@ const generateFallbackAvatar = (username: string): string => { // Function to determine primary role based on repositories const getPrimaryRole = (repositories: string[], contributionScore: number, username: string): string => { - if (repositories.length === 0) return 'Contributor'; + if (!repositories || repositories.length === 0) return 'Contributor'; // Special roles for UTCP admins - const utcpAdmins = ['h3xxit', 'aliraza1006', 'edujuan', 'ulughbeck', 'AndreiGS']; - if (utcpAdmins.includes(username)) { + if (SPECIAL_CONTRIBUTORS.ADMINS.includes(username)) { return 'UTCP Admin'; } - // Special role for Kamil - if (username === 'Raezil') { + // Special role for lead developer + if (username === SPECIAL_CONTRIBUTORS.LEAD_DEVELOPER) { return 'Lead Maintainer of Go Port'; } @@ -134,49 +157,92 @@ const getPrimaryRole = (repositories: string[], contributionScore: number, usern // Transform GitHub contributor data to display format const transformContributor = (ghContributor: GitHubContributor): DisplayContributor => { - const status = getContributorStatus(ghContributor.impact_score, ghContributor.last_activity, ghContributor.login); - const role = getPrimaryRole(ghContributor.repositories, ghContributor.impact_score, ghContributor.login); - const avatar = ghContributor.avatar_url || generateFallbackAvatar(ghContributor.login); - const joinDate = formatDate(ghContributor.created_at); + // Add null safety checks for all operations + const impact_score = ghContributor?.impact_score || 0; + const repositories = ghContributor?.repositories || []; + const login = ghContributor?.login || 'unknown'; + + const status = getContributorStatus(impact_score, ghContributor?.last_activity || null, login); + const role = getPrimaryRole(repositories, impact_score, login); + const avatar = ghContributor?.avatar_url || generateFallbackAvatar(login); + const joinDate = formatDate(ghContributor?.created_at || null); // Determine top contribution based on repositories - const topContribution = ghContributor.repo_count > 1 - ? `Cross-project development (${ghContributor.repo_count} repos)` - : ghContributor.repositories[0] || 'Code contributions'; + const repo_count = ghContributor?.repo_count || 0; + const topContribution = repo_count > 1 + ? `Cross-project development (${repo_count} repos)` + : repositories[0] || 'Code contributions'; + + // Format line changes summary + const formatLineChanges = (additions: number, deletions: number, total: number) => { + if (total === 0) return 'No code changes tracked'; + + const additionsStr = additions.toLocaleString(); + const deletionsStr = deletions.toLocaleString(); + const totalStr = total.toLocaleString(); + + return `+${additionsStr} -${deletionsStr} (${totalStr} total)`; + }; + + // Format recent activity (now showing line changes instead of just commit count) + const total_changes = ghContributor?.total_changes || 0; + const total_additions = ghContributor?.total_additions || 0; + const total_deletions = ghContributor?.total_deletions || 0; + const total_recent_commits = ghContributor?.total_recent_commits || 0; - // Format recent activity - const recentActivity = ghContributor.total_recent_commits > 0 - ? `${ghContributor.total_recent_commits} recent commits` + const recentActivity = total_changes > 0 + ? formatLineChanges(total_additions, total_deletions, total_changes) + : total_recent_commits > 0 + ? `${total_recent_commits} recent commits` : 'No recent activity'; - // Format quality metrics + // Format quality metrics with enhanced commit info + const total_prs = ghContributor?.total_prs || 0; + const total_reviews = ghContributor?.total_reviews || 0; + const total_commits = ghContributor?.total_commits || 0; + const pr_success_rate = ghContributor?.pr_success_rate || 0; + const qualityMetrics = [ - ghContributor.total_prs > 0 ? `${ghContributor.total_prs} PRs (${ghContributor.pr_success_rate}% merged)` : null, - ghContributor.total_reviews > 0 ? `${ghContributor.total_reviews} reviews` : null + total_prs > 0 ? `${total_prs} PRs (${pr_success_rate}% merged)` : null, + total_reviews > 0 ? `${total_reviews} reviews` : null, + total_commits > 0 ? `${total_commits} total commits` : null ].filter(Boolean).join(' β€’ ') || 'Building great code'; // Add star badge for high contributors const badges: Array<{ icon: string; tooltip: string }> = []; - if (ghContributor.impact_score >= 100) { + if (impact_score >= CONTRIBUTOR_STATUS_THRESHOLDS.CORE_CONTRIBUTOR) { badges.push({ icon: "⭐", tooltip: "High Contributor" }); } return { - id: ghContributor.id, - name: ghContributor.name || ghContributor.login, - username: ghContributor.login, - githubUsername: ghContributor.login, + id: ghContributor?.id || 0, + name: ghContributor?.name || login, + username: login, + githubUsername: login, role, status, - contributions: ghContributor.contributions, - impact_score: ghContributor.impact_score, + contributions: ghContributor?.contributions || 0, + impact_score, avatar, joinDate, topContribution, recentActivity, qualityMetrics, - lookingForJob: ghContributor.hireable, - total_recent_commits: ghContributor.total_recent_commits, + lookingForJob: ghContributor?.hireable || false, + repositories, + total_recent_commits, + // Enhanced line change statistics + total_additions, + total_deletions, + total_changes, + total_commits: total_commits || total_recent_commits, + commits_analyzed: ghContributor?.commits_analyzed || 0, + // PR and review statistics for tooltips + total_prs, + total_merged_prs: ghContributor?.total_merged_prs || 0, + total_reviews, + pr_success_rate, + lineChangeSummary: recentActivity, badges: badges.length > 0 ? badges : undefined }; }; @@ -198,6 +264,16 @@ const fallbackContributors: DisplayContributor[] = [ recentActivity: "Loading...", qualityMetrics: "Loading...", total_recent_commits: 0, + total_additions: 0, + total_deletions: 0, + total_changes: 0, + total_commits: 0, + commits_analyzed: 0, + total_prs: 0, + total_merged_prs: 0, + total_reviews: 0, + pr_success_rate: 0, + lineChangeSummary: "Loading...", lookingForJob: false } ]; @@ -214,25 +290,43 @@ const useContributors = (): DisplayContributor[] => { try { const autoModule = await import('../data/contributors.json'); const autoContributorsData = autoModule.default as ContributorsData; - allContributors = [...allContributors, ...autoContributorsData.contributors]; + if (autoContributorsData?.contributors && Array.isArray(autoContributorsData.contributors)) { + allContributors = [...allContributors, ...autoContributorsData.contributors]; + } else { + console.warn('Auto-generated contributors data has invalid format'); + } } catch (error) { - console.warn('Could not load auto-generated contributors data from GitHub.', error); + console.warn('Could not load auto-generated contributors data from GitHub:', error instanceof Error ? error.message : 'Unknown error'); } - // Load manually added contributors - try { - const manualModule = await import('../data/contributors-manual.json'); - const manualContributorsData = manualModule.default as ContributorsData; - allContributors = [...allContributors, ...manualContributorsData.contributors]; - } catch (error) { - console.warn('Could not load manual contributors data.', error); - } + // Load manually added contributors (temporarily disabled) + // try { + // const manualModule = await import('../data/contributors-manual.json'); + // const manualContributorsData = manualModule.default as ContributorsData; + // if (manualContributorsData?.contributors && Array.isArray(manualContributorsData.contributors)) { + // allContributors = [...allContributors, ...manualContributorsData.contributors]; + // } else { + // console.warn('Manual contributors data has invalid format'); + // } + // } catch (error) { + // console.warn('Could not load manual contributors data:', error instanceof Error ? error.message : 'Unknown error'); + // } // If we have any contributors, transform and sort them if (allContributors.length > 0) { const transformedContributors = allContributors .map(transformContributor) - .sort((a, b) => b.impact_score - a.impact_score); // Sort by impact score descending + .sort((a, b) => { + // Sort by total changes first (if available), then by impact score as fallback + const aChanges = a.total_changes || 0; + const bChanges = b.total_changes || 0; + + if (aChanges !== bChanges) { + return bChanges - aChanges; // Sort by lines changed descending + } + + return b.impact_score - a.impact_score; // Fallback to impact score + }); setContributors(transformedContributors); } else { // No contributors loaded, show fallback message @@ -251,6 +345,17 @@ const useContributors = (): DisplayContributor[] => { recentActivity: "N/A", qualityMetrics: "N/A", total_recent_commits: 0, + total_additions: 0, + total_deletions: 0, + total_changes: 0, + total_commits: 0, + commits_analyzed: 0, + total_prs: 0, + total_merged_prs: 0, + total_reviews: 0, + pr_success_rate: 0, + repositories: [], + lineChangeSummary: "N/A", lookingForJob: false }]); } @@ -262,74 +367,99 @@ const useContributors = (): DisplayContributor[] => { return contributors; }; -const ContributorRow = ({ contributor, rank }: { contributor: DisplayContributor; rank: number }) => ( -
-
-
- {contributor.avatar.startsWith('http') ? ( - {`${contributor.name} { - const target = e.target as HTMLImageElement; - target.style.display = 'none'; - target.parentElement!.textContent = generateFallbackAvatar(contributor.username); - }} - style={{ - width: '100%', - height: '100%', - borderRadius: '8px', - objectFit: 'cover' - }} - /> - ) : ( - contributor.avatar - )} -
-
-

- {rank}. - - {contributor.name} - -

-

- {contributor.role} -

-

- {contributor.status} -

+const ContributorRow = ({ contributor, rank }: { contributor: DisplayContributor; rank: number }) => { + const [imageError, setImageError] = useState(false); + + const handleImageError = useCallback(() => { + setImageError(true); + }, []); + + const isHttpAvatar = contributor.avatar?.startsWith('http') ?? false; + const shouldShowImage = isHttpAvatar && !imageError; + + return ( +
+
+
+ {shouldShowImage ? ( + {`${contributor.name} + ) : ( + + {imageError ? generateFallbackAvatar(contributor.username) : contributor.avatar} + + )} +
+
+

+ {rank}. + + {contributor.name} + +

+

+ {contributor.role} +

+

+ {contributor.status} +

+
-
- -
- {contributor.lookingForJob && ( - - )} - {contributor.badges && ( -
- {contributor.badges.map((badge, index) => ( -
- {badge.icon} -
- ))} + +
+ {contributor.lookingForJob && ( + + )} + {contributor.badges && ( +
+ {contributor.badges.map((badge, index) => ( +
+ {badge.icon} +
+ ))} +
+ )} +
+ 0 + ? `${contributor.lineChangeSummary || 'No line changes'} β€’ ${contributor.total_commits || 0} total commits β€’ ${contributor.total_prs || 0} PRs (${contributor.pr_success_rate || 0}% merged)` + : `${contributor.total_recent_commits || 0} commits in last 6 months β€’ ${contributor.total_prs || 0} PRs β€’ ${contributor.total_reviews || 0} reviews` + }> + {contributor.total_changes > 0 ? ( +
+ + {contributor.total_changes >= 1000 + ? `${Math.round(contributor.total_changes / 1000)}k` + : contributor.total_changes.toLocaleString() + } + + lines changed +
+ ) : ( +
+ + {contributor.total_recent_commits} + + commits +
+ )} +
- )} -
- - {contributor.impact_score} -
-
-); + ); +}; export default function Contributors(): React.ReactNode { const contributors = useContributors(); @@ -348,7 +478,7 @@ export default function Contributors(): React.ReactNode { We are extremely grateful for each and every one of you!

- Ranked by contribution score: recent activity across all repositories + Ranked by total lines changed across all repositories

@@ -363,6 +493,33 @@ export default function Contributors(): React.ReactNode { /> ))}
+ + {contributors.length > 1 && contributors[0]?.total_changes > 0 && ( +
+

πŸ“Š Community Impact

+
+
+ + {contributors.reduce((sum, c) => sum + (c?.total_changes || 0), 0).toLocaleString()} + + Total Lines Changed +
+ +
+ + {contributors.reduce((sum, c) => sum + (c?.total_commits || 0), 0).toLocaleString()} + + Total Commits +
+
+ + {new Set(contributors.flatMap(c => c?.repositories || [])).size} + + Active Repositories +
+
+
+ )}
diff --git a/src/types/contributors.ts b/src/types/contributors.ts index a211700..f92c840 100644 --- a/src/types/contributors.ts +++ b/src/types/contributors.ts @@ -21,9 +21,15 @@ export interface GitHubContributor { total_prs: number; total_merged_prs: number; total_recent_commits: number; + total_commits: number; total_reviews: number; last_activity: string | null; pr_success_rate: number; + // Line change statistics + total_additions: number; + total_deletions: number; + total_changes: number; + commits_analyzed: number; } export interface ContributorsData { @@ -33,6 +39,11 @@ export interface ContributorsData { total_impact_score: number; total_recent_activity: number; scoring_method: string; + // Enhanced aggregated line change statistics + total_additions: number; + total_deletions: number; + total_changes: number; + total_commits_analyzed: number; contributors: GitHubContributor[]; } @@ -49,10 +60,25 @@ export interface DisplayContributor { joinDate: string; topContribution: string; lookingForJob: boolean; + // Repository data + repositories?: string[]; // Additional scoring display data recentActivity?: string; qualityMetrics?: string; total_recent_commits: number; + // Line change statistics for display + total_additions: number; + total_deletions: number; + total_changes: number; + total_commits: number; + commits_analyzed: number; + // PR and review statistics for tooltips + total_prs: number; + total_merged_prs: number; + total_reviews: number; + pr_success_rate: number; + // Formatted line change display + lineChangeSummary?: string; badges?: Array<{ icon: string; tooltip: string; From fb7f134cd00b0fc5d04a112e215f9466af62e905 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Thu, 4 Sep 2025 12:56:19 +0200 Subject: [PATCH 2/5] Update scripts/fetch-contributors.js Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- scripts/fetch-contributors.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetch-contributors.js b/scripts/fetch-contributors.js index 5793eed..7bf226b 100644 --- a/scripts/fetch-contributors.js +++ b/scripts/fetch-contributors.js @@ -151,7 +151,7 @@ const fetchUserPRsAndActivity = async (username, repoName) => { let hasMore = true; console.log(` πŸ“Š Fetching all commits for line statistics...`); - while (hasMore && allCommits.length < 500) { // Limit to prevent excessive API calls + while (hasMore && allCommits.length < (BATCH_CONFIG.COMMIT_ANALYSIS_LIMIT || Infinity)) { // Limit to prevent excessive API calls try { const commitPage = await githubApi(`/repos/${ORG_NAME}/${repoName}/commits?author=${username}&per_page=100&page=${page}`); From d5c8ff62bc840bc7f068a677df5283dfc74434f0 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:00:53 +0200 Subject: [PATCH 3/5] Fix 0 id --- src/pages/hall-of-fame.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pages/hall-of-fame.tsx b/src/pages/hall-of-fame.tsx index 35ee8f7..df7da66 100644 --- a/src/pages/hall-of-fame.tsx +++ b/src/pages/hall-of-fame.tsx @@ -213,9 +213,18 @@ const transformContributor = (ghContributor: GitHubContributor): DisplayContribu if (impact_score >= CONTRIBUTOR_STATUS_THRESHOLDS.CORE_CONTRIBUTOR) { badges.push({ icon: "⭐", tooltip: "High Contributor" }); } + + const generateHash = (string) => { + let hash = 0; + for (const char of string) { + hash = (hash << 5) - hash + char.charCodeAt(0); + hash |= 0; // Constrain to 32bit integer + } + return hash; + }; return { - id: ghContributor?.id || 0, + id: ghContributor?.id || generateHash(login), name: ghContributor?.name || login, username: login, githubUsername: login, From 499bdf9c7ad0e8cb8b8c0454959f8e093666fe8b Mon Sep 17 00:00:00 2001 From: "Juan V." Date: Fri, 5 Sep 2025 11:30:22 +0200 Subject: [PATCH 4/5] add special contributors --- src/data/contributors-manual.json | 60 +++++++++++++++++++++- src/pages/hall-of-fame.module.css | 84 +++++++++++++++++++++++++++++++ src/pages/hall-of-fame.tsx | 66 ++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) diff --git a/src/data/contributors-manual.json b/src/data/contributors-manual.json index ff4c6b8..5f2252d 100644 --- a/src/data/contributors-manual.json +++ b/src/data/contributors-manual.json @@ -1,6 +1,6 @@ { "generated_at": "2025-09-03T13:46:04.752Z", - "total_contributors": 1, + "total_contributors": 3, "total_contributions": 24, "total_impact_score": 24, "total_recent_activity": 24, @@ -12,7 +12,7 @@ "name": "Roberto Catalano", "avatar_url": "https://avatars.githubusercontent.com/u/999999999?v=4", "html_url": "https://github.com/Robobc", - "bio": "Solutions Architect at AWS in Zurich", + "bio": "Created some great example repositories and helped with the documentation.", "company": "Amazon Web Services", "location": "ZΓΌrich", "blog": null, @@ -33,6 +33,62 @@ "total_reviews": 0, "last_activity": "2025-09-03T13:46:04Z", "pr_success_rate": 0 + }, + { + "id": 0, + "login": "gwoodwa1", + "name": "Gary Woodward", + "avatar_url": "https://github.com/gwoodwa1.png", + "html_url": "https://github.com/gwoodwa1", + "bio": "Supported Go-port by giving gnmi support over grpc transport", + "company": null, + "location": null, + "blog": null, + "hireable": null, + "public_repos": 0, + "followers": 0, + "following": 0, + "created_at": null, + "contributions": 0, + "repositories": [ + "utcp-go" + ], + "repo_count": 1, + "impact_score": 0, + "total_prs": 0, + "total_merged_prs": 0, + "total_recent_commits": 0, + "total_reviews": 0, + "last_activity": null, + "pr_success_rate": 0 + }, + { + "id": 0, + "login": "armanzeroeight", + "name": "Arman Nourifar", + "avatar_url": "https://github.com/armanzeroeight.png", + "html_url": "https://github.com/armanzeroeight", + "bio": "For social media content and his excellent gifs", + "company": null, + "location": "Oslo, Norway", + "blog": "https://medium.com/@arman08", + "hireable": null, + "public_repos": 0, + "followers": 0, + "following": 0, + "created_at": null, + "contributions": 0, + "repositories": [ + "community" + ], + "repo_count": 1, + "impact_score": 0, + "total_prs": 0, + "total_merged_prs": 0, + "total_recent_commits": 0, + "total_reviews": 0, + "last_activity": null, + "pr_success_rate": 0 } ] } diff --git a/src/pages/hall-of-fame.module.css b/src/pages/hall-of-fame.module.css index f2e79a2..c0ef90b 100644 --- a/src/pages/hall-of-fame.module.css +++ b/src/pages/hall-of-fame.module.css @@ -843,4 +843,88 @@ .linesChangedLabel { font-size: 0.4rem; } +} + +/* Other Contributors (non-coder) section */ +.otherContributorsSection { + padding: 3rem 1rem 0; + max-width: 1200px; + margin: 0 auto; +} + +.otherContributorsHeader { + text-align: center; + margin-bottom: 1.5rem; +} + +.otherContributorsTitle { + font-size: 2rem; + font-weight: 700; + margin: 0 0 0.5rem 0; + background: linear-gradient(135deg, #ffffff 0%, #8b5cf6 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.otherContributorsSubtitle { + font-size: 1rem; + color: #888888; + margin: 0; +} + +.otherContributorsGrid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1rem; +} + +.otherContributorCard { + background: linear-gradient(145deg, #111111 0%, #0a0a0a 100%); + border: 1px solid #222222; + border-radius: 12px; + padding: 1rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.otherContributorIcon { + width: 72px; + height: 72px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%); + border: 1px solid #333333; +} + +.otherContributorImage { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + +.otherContributorRole { + font-size: 1.05rem; + font-weight: 600; + margin: 0.25rem 0 0; + color: #ffffff; +} + +.otherContributorThanks { + font-size: 0.9rem; + color: #cccccc; + margin: 0; + text-align: center; +} + +@media (max-width: 768px) { + .otherContributorsSection { + padding-top: 2rem; + } } \ No newline at end of file diff --git a/src/pages/hall-of-fame.tsx b/src/pages/hall-of-fame.tsx index 35ee8f7..88bd33b 100644 --- a/src/pages/hall-of-fame.tsx +++ b/src/pages/hall-of-fame.tsx @@ -463,6 +463,60 @@ const ContributorRow = ({ contributor, rank }: { contributor: DisplayContributor export default function Contributors(): React.ReactNode { const contributors = useContributors(); + const [otherContributors, setOtherContributors] = useState>([]); + + useEffect(() => { + const loadOtherContributors = async () => { + try { + const manualModule = await import('../data/contributors-manual.json'); + const manualData = manualModule.default as { contributors?: Array }; + const mapped = (manualData?.contributors || []).map((c: any) => { + const username: string = c?.login || c?.name || 'friend'; + const name: string = c?.name || username; + const bio: string | null = c?.bio || null; + const avatar: string | undefined = c?.avatar_url || undefined; + return { + avatar, + role: name, + thanks: bio || 'Thank you for supporting UTCP.', + username + }; + }); + setOtherContributors(mapped); + } catch (err) { + // Fallback to a small static list when manual file is unavailable + setOtherContributors([ + { avatar: undefined, role: 'Maintainer', thanks: 'Guiding the project and ensuring high quality.', username: 'maintainer' }, + { avatar: undefined, role: 'Designer', thanks: 'Improving UX and visual language.', username: 'designer' }, + { avatar: undefined, role: 'Researcher', thanks: 'Exploring the landscape and informing decisions.', username: 'researcher' } + ]); + } + }; + loadOtherContributors(); + }, []); + + const OtherContributorCard = ({ avatar, role, thanks, username }: { avatar?: string; role: string; thanks: string; username: string }) => { + const [imageError, setImageError] = useState(false); + const showImage = Boolean(avatar) && !imageError; + return ( +
+
+ {showImage ? ( + {`${role} setImageError(true)} + /> + ) : ( + {generateFallbackAvatar(username)} + )} +
+
{role}
+
{thanks}
+
+ ); + }; return ( ))}
+ +
+
+

Special Thanks

+

Beyond code, here are some of our champions

+
+
+ {otherContributors.map((oc, i) => ( + + ))} +
+
{contributors.length > 1 && contributors[0]?.total_changes > 0 && (
From 60a0702058c2a0cac474c5ba289b3cada07aeeb3 Mon Sep 17 00:00:00 2001 From: "Juan V." Date: Fri, 5 Sep 2025 11:36:48 +0200 Subject: [PATCH 5/5] updpated --- src/data/contributors-manual.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/contributors-manual.json b/src/data/contributors-manual.json index 5f2252d..7cd5f62 100644 --- a/src/data/contributors-manual.json +++ b/src/data/contributors-manual.json @@ -7,7 +7,7 @@ "scoring_method": "manual_entry", "contributors": [ { - "id": 999999999, + "id": 999999999, "login": "Robobc", "name": "Roberto Catalano", "avatar_url": "https://avatars.githubusercontent.com/u/999999999?v=4",