From 580e6ee883ab22968e9c21a513049d07ef8fe043 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:17:04 +0200 Subject: [PATCH 1/9] Add Langchain adapter maintainters --- about/about-us.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/about/about-us.md b/about/about-us.md index f779d20..7f971e7 100644 --- a/about/about-us.md +++ b/about/about-us.md @@ -30,9 +30,11 @@ We believe in the power of open source collaboration. This project thrives on co ### Go Port Maintainer - Kamil MoΕ›ciszko ([LinkedIn](https://www.linkedin.com/in/kamilm97/), kmosc@protonmail.com) -### Contributors +### Langchain Utcp Adapters Maintainter - Luca Perrozzi ([LinkedIn](https://www.linkedin.com/in/luca-perrozzi/)) - Roberto Catalano ([LinkedIn](https://www.linkedin.com/in/roberto-catalano-5b7793123/), [GitHub](https://github.com/Robobc)) + +### Contributors - Tiago Prelato ([LinkedIn](https://www.linkedin.com/in/tiago-prelato-257787210/), [X](https://x.com/SneyX_)) - Bruce Miao From cbd1b2bd13449185b62b28f07d007ccee64319b2 Mon Sep 17 00:00:00 2001 From: "Juan V." Date: Wed, 3 Sep 2025 17:01:35 +0200 Subject: [PATCH 2/9] hall of fame v1 --- .github/workflows/update-contributors.yml | 65 +++ CONTRIBUTORS.md | 185 ++++++++ about/about-us.md | 2 + docusaurus.config.ts | 4 + package.json | 6 +- scripts/fetch-contributors.js | 340 ++++++++++++++ src/data/contributors-manual.json | 38 ++ src/data/contributors.json | 307 ++++++++++++ src/pages/hall-of-fame.module.css | 547 ++++++++++++++++++++++ src/pages/hall-of-fame.tsx | 395 ++++++++++++++++ src/types/contributors.ts | 60 +++ src/types/json-modules.d.ts | 4 + 12 files changed, 1951 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/update-contributors.yml create mode 100644 CONTRIBUTORS.md create mode 100644 scripts/fetch-contributors.js create mode 100644 src/data/contributors-manual.json create mode 100644 src/data/contributors.json create mode 100644 src/pages/hall-of-fame.module.css create mode 100644 src/pages/hall-of-fame.tsx create mode 100644 src/types/contributors.ts create mode 100644 src/types/json-modules.d.ts diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml new file mode 100644 index 0000000..ec67a3c --- /dev/null +++ b/.github/workflows/update-contributors.yml @@ -0,0 +1,65 @@ +name: Update Contributors Data + +on: + schedule: + # Run every day at 6 AM UTC + - cron: '0 6 * * *' + workflow_dispatch: # Allow manual triggering + push: + branches: [ main ] + paths: + - 'scripts/fetch-contributors.js' + +jobs: + update-contributors: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Fetch contributors data + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "πŸš€ Starting contributor data fetch..." + node scripts/fetch-contributors.js + echo "βœ… Contributor data fetch completed" + + - name: Check if contributors data changed + id: verify-changed-files + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "changed=true" >> $GITHUB_OUTPUT + else + echo "changed=false" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: steps.verify-changed-files.outputs.changed == 'true' + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add src/data/contributors.json + git commit -m "chore: update contributors data $(date +'%Y-%m-%d')" + git push + echo "βœ… Contributors data updated and pushed to repository" + + - name: Workflow Summary + run: | + if [[ "${{ steps.verify-changed-files.outputs.changed }}" == "true" ]]; then + echo "πŸ“Š Contributors data was updated and the site will be redeployed automatically" + else + echo "ℹ️ Contributors data unchanged - no updates needed" + fi diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..291b013 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,185 @@ +# Contributors Data + +This project automatically fetches and displays real contributors from the entire `universal-tool-calling-protocol` GitHub organization. + +## How it Works + +1. **Data Fetching**: The `scripts/fetch-contributors.js` script queries the GitHub API to: + - Get all repositories in the `universal-tool-calling-protocol` organization + - Fetch contributors for each repository + - **Collect detailed activity metrics**: PRs, reviews, recent commits, last activity dates + - Aggregate contribution counts across all repositories + - Enhance data with user profiles from GitHub + - Calculate hybrid impact scores for each contributor + +2. **Data Processing**: The script transforms GitHub API data into a format suitable for the contributors page, including: + - Automatic role detection based on repository names (TypeScript SDK, Python SDK, etc.) + - **Hybrid impact scoring** combining recent activity, overall contributions, code quality, and multi-project involvement + - Status determination based on impact scores and activity recency + - Real GitHub profile images with emoji fallbacks for errors + - Badge assignment for notable contributors based on multiple metrics + +3. **Automatic Updates**: GitHub Actions automatically updates the contributors data: + - **Daily**: Runs at 6 AM UTC every day + - **Manual**: Can be triggered via workflow_dispatch + - **On Changes**: Runs when the fetch script is updated + +## Setup + +### Environment Variables + +For local development, you can set a GitHub token to avoid rate limiting: + +```bash +export GITHUB_TOKEN=your_github_token_here +npm run fetch-contributors +``` + +### Running Locally + +```bash +# Fetch contributors data +npm run fetch-contributors + +# Build site with fresh data +npm run build + +# Build without fetching data (faster) +npm run build:fast +``` + +### Manual Setup + +If you need to run the script manually: + +```bash +node scripts/fetch-contributors.js +``` + +## Data Structure + +The generated `src/data/contributors.json` file contains: + +```json +{ + "generated_at": "2024-01-01T12:00:00.000Z", + "total_contributors": 25, + "total_contributions": 1500, + "total_impact_score": 3250, + "total_recent_activity": 145, + "scoring_method": "hybrid_impact_score", + "contributors": [ + { + "id": 123456, + "login": "username", + "name": "Real Name", + "avatar_url": "https://github.com/avatar", + "html_url": "https://github.com/username", + "bio": "Developer bio", + "company": "Company Name", + "location": "City, Country", + "blog": "https://blog.com", + "hireable": true, + "public_repos": 50, + "followers": 100, + "following": 80, + "created_at": "2020-01-01T00:00:00Z", + "contributions": 150, + "repositories": ["repo1", "repo2"], + "repo_count": 2, + "impact_score": 185, + "total_prs": 15, + "total_merged_prs": 12, + "total_recent_commits": 23, + "total_reviews": 8, + "last_activity": "2024-01-15T14:30:00Z", + "pr_success_rate": 80 + } + ] +} +``` + +## Contributor Display Logic + +### Simplified Impact Scoring System + +Contributors are ranked using a **simplified impact score** based only on recent activity across all repositories: + +#### Scoring Formula +``` +Impact Score = Recent Activity Γ— Recency Factor +``` + +Where Recent Activity is the sum of commits across all repositories in the last 6 months. + +#### Recency Weighting +Recent activity receives higher weight to prioritize active contributors: +- **Last 30 days**: 100% weight (1.0x multiplier) +- **Last 3 months**: 80% weight (0.8x multiplier) +- **Last 6 months**: 50% weight (0.5x multiplier) +- **Last year**: 30% weight (0.3x multiplier) +- **Older**: 10% weight (0.1x multiplier) + +### Roles +Roles are automatically determined based on repository patterns: +- `typescript|js-sdk` β†’ "TypeScript/JavaScript SDK" +- `python|py-sdk` β†’ "Python SDK" +- `go-sdk|golang` β†’ "Go SDK" +- `rust-sdk` β†’ "Rust SDK" +- `java-sdk` β†’ "Java SDK" +- `csharp|dotnet` β†’ "C# SDK" +- `specification|docs` β†’ "Specification & Docs" +- Multiple repos β†’ "Multi-language SDK" + +### Status Levels (Impact Score Based) +- **Core contributor**: 100+ impact score + recent activity +- **Core contributor (inactive)**: 100+ impact score but no recent activity +- **Active contributor**: 50+ impact score + recent activity +- **Regular contributor**: 20+ impact score or inactive but previously active +- **New contributor**: <20 impact score + +### Enhanced Badge System +- πŸ† **Top Impact Contributor**: 200+ impact score +- ⭐ **High Impact Contributor**: 100+ impact score +- πŸ”₯ **Multi-project Contributor**: 3+ repositories +- πŸš€ **Recently Active**: 10+ commits in last 6 months +- ✨ **Quality Contributor**: 80%+ PR merge rate with 5+ PRs + +### Data Collection & Metrics +For each contributor, the system collects: +- **Pull Requests**: Total PRs created and merge success rate +- **Code Reviews**: Participation in reviewing others' code +- **Recent Activity**: Commits in the last 6 months +- **Cross-project Work**: Contributions across multiple repositories +- **Last Activity Date**: Most recent contribution timestamp + +### Hiring Status +Contributors with `hireable: true` in their GitHub profile show an "Open to work" badge. + +## Advantages of Simplified Scoring + +The simplified impact scoring system provides several benefits: + +1. **Recency Focus**: Active contributors rank higher than inactive ones +2. **Cross-project Aggregation**: Recent commits are summed across all repositories +3. **Simple and Fair**: Transparent calculation based purely on recent activity +4. **Prevents Gaming**: Cannot be inflated through historical contributions or metadata manipulation + +## Troubleshooting + +### Rate Limiting +The enhanced scoring system makes significantly more API calls (PRs, commits, reviews per contributor per repo). Without authentication, GitHub API limits to 60 requests/hour. The script includes delays to avoid hitting limits, but **using `GITHUB_TOKEN` is highly recommended** for reasonable execution times. + +**Expected Runtime**: +- Without token: 15-30+ minutes (depending on contributor count) +- With token: 2-5 minutes (much faster due to higher rate limits) + +### Missing Data +If the contributors page shows "Contributors data unavailable": +1. Run `npm run fetch-contributors` locally +2. Check if `src/data/contributors.json` was created +3. Ensure the GitHub API is accessible +4. Check console for error messages + +### Build Integration +The build process automatically fetches fresh data. For faster builds during development, use `npm run build:fast` to skip the data fetch. diff --git a/about/about-us.md b/about/about-us.md index f779d20..6fb50dc 100644 --- a/about/about-us.md +++ b/about/about-us.md @@ -18,6 +18,8 @@ We believe in the power of open source collaboration. This project thrives on co ## Contributors +The UTCP project is made possible by the dedication and contributions of our amazing community. For a complete and up-to-date list of all contributors with their activity rankings, visit our [**Hall of Fame**](/hall-of-fame) page. + ### UTCP Admin - Razvan-Ion Radulescu ([Bevel Software](https://www.bevel.software), razvan.radulescu@bevel.software) - Andrei-Stefan Ghiurtu ([LinkedIn](https://www.linkedin.com/in/andrei-stefan-ghiurtu/)) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 653f5c4..ad26c8f 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -246,6 +246,10 @@ const config: Config = { label: 'About Us', to: '/about/about-us', }, + { + label: 'Hall of Fame', + to: '/hall-of-fame', + }, { label: 'RFC', to: '/about/RFC', diff --git a/package.json b/package.json index eb7ec2c..73ada4b 100644 --- a/package.json +++ b/package.json @@ -5,14 +5,16 @@ "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", - "build": "docusaurus build", + "build": "node scripts/fetch-contributors.js && docusaurus build", + "build:fast": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", "clear": "docusaurus clear", "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "typecheck": "tsc" + "typecheck": "tsc", + "fetch-contributors": "node scripts/fetch-contributors.js" }, "dependencies": { "@docusaurus/core": "3.8.1", diff --git a/scripts/fetch-contributors.js b/scripts/fetch-contributors.js new file mode 100644 index 0000000..8478a21 --- /dev/null +++ b/scripts/fetch-contributors.js @@ -0,0 +1,340 @@ +#!/usr/bin/env node + +const fs = require('fs'); +const path = require('path'); + +const GITHUB_TOKEN = process.env.GITHUB_TOKEN; +const ORG_NAME = 'universal-tool-calling-protocol'; +const OUTPUT_FILE = path.join(__dirname, '../src/data/contributors.json'); + +const githubApi = async (endpoint) => { + const headers = { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'UTCP-Spec-Site' + }; + + if (GITHUB_TOKEN) { + headers['Authorization'] = `token ${GITHUB_TOKEN}`; + } + + const response = await fetch(`https://api.github.com${endpoint}`, { headers }); + + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); + } + + return response.json(); +}; + +const fetchAllRepositories = async () => { + try { + console.log(`Fetching repositories for ${ORG_NAME}...`); + const repos = await githubApi(`/orgs/${ORG_NAME}/repos?type=all&per_page=100`); + return repos.filter(repo => !repo.archived && !repo.disabled); + } catch (error) { + console.error('Error fetching repositories:', error); + return []; + } +}; + +const fetchContributorsForRepo = async (repoName) => { + try { + const contributors = await githubApi(`/repos/${ORG_NAME}/${repoName}/contributors?per_page=100`); + return contributors.map(contributor => ({ + ...contributor, + repo: repoName + })); + } catch (error) { + console.warn(`Could not fetch contributors for ${repoName}:`, error.message); + return []; + } +}; + +const fetchUserPRsAndActivity = async (username, repoName) => { + try { + // Get PRs for this user in this repo + const prs = await githubApi(`/repos/${ORG_NAME}/${repoName}/pulls?state=all&creator=${username}&per_page=100`); + + // Get recent commits (last 6 months) for recency calculation + const sixMonthsAgo = new Date(); + sixMonthsAgo.setMonth(sixMonthsAgo.getMonth() - 6); + const commits = await githubApi(`/repos/${ORG_NAME}/${repoName}/commits?author=${username}&since=${sixMonthsAgo.toISOString()}&per_page=100`); + + // 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); + + return { + prs: prs.length, + mergedPrs: prs.filter(pr => pr.merged_at).length, + recentCommits: commits.length, + reviews: userReviews.length, + lastActivity: commits.length > 0 ? commits[0].commit.author.date : null + }; + } catch (error) { + console.warn(`Could not fetch detailed activity for ${username} in ${repoName}:`, error.message); + return { + prs: 0, + mergedPrs: 0, + recentCommits: 0, + reviews: 0, + lastActivity: null + }; + } +}; + +const fetchUserDetails = async (username) => { + try { + return await githubApi(`/users/${username}`); + } catch (error) { + console.warn(`Could not fetch details for user ${username}:`, error.message); + return null; + } +}; + +const aggregateContributors = (contributorsByRepo) => { + const contributorMap = new Map(); + + contributorsByRepo.flat().forEach(contributor => { + if (contributor.type === 'Bot') return; // Skip bots + + const existing = contributorMap.get(contributor.login); + if (existing) { + existing.contributions += contributor.contributions; + existing.repositories.add(contributor.repo); + // Aggregate activity data + if (contributor.activityData) { + existing.totalPrs += contributor.activityData.prs; + existing.totalMergedPrs += contributor.activityData.mergedPrs; + existing.totalRecentCommits += contributor.activityData.recentCommits; + existing.totalReviews += contributor.activityData.reviews; + // Keep the most recent activity date + if (contributor.activityData.lastActivity) { + const activityDate = new Date(contributor.activityData.lastActivity); + if (!existing.lastActivity || activityDate > new Date(existing.lastActivity)) { + existing.lastActivity = contributor.activityData.lastActivity; + } + } + } + } else { + contributorMap.set(contributor.login, { + login: contributor.login, + id: contributor.id, + avatar_url: contributor.avatar_url, + html_url: contributor.html_url, + contributions: contributor.contributions, + repositories: new Set([contributor.repo]), + totalPrs: contributor.activityData?.prs || 0, + totalMergedPrs: contributor.activityData?.mergedPrs || 0, + totalRecentCommits: contributor.activityData?.recentCommits || 0, + totalReviews: contributor.activityData?.reviews || 0, + lastActivity: contributor.activityData?.lastActivity || null + }); + } + }); + + return Array.from(contributorMap.values()).map(contributor => ({ + ...contributor, + repositories: Array.from(contributor.repositories) + })); +}; + +// Calculate simplified score based on recent activity only +const calculateContributorScore = (contributor) => { + const now = new Date(); + const lastActivityDate = contributor.lastActivity ? new Date(contributor.lastActivity) : null; + + // Calculate recency factor (days since last activity) + let recencyFactor = 0.1; // Default for very old or no activity + if (lastActivityDate) { + const daysSinceLastActivity = (now - lastActivityDate) / (1000 * 60 * 60 * 24); + if (daysSinceLastActivity <= 30) recencyFactor = 1.0; // Last 30 days: full points + else if (daysSinceLastActivity <= 90) recencyFactor = 0.8; // Last 3 months: 80% + else if (daysSinceLastActivity <= 180) recencyFactor = 0.5; // Last 6 months: 50% + else if (daysSinceLastActivity <= 365) recencyFactor = 0.3; // Last year: 30% + else recencyFactor = 0.1; // Older: 10% + } + + // Simplified score: only recent activity weighted by recency + const score = Math.round(contributor.totalRecentCommits * recencyFactor); + + return Math.max(score, 1); // Minimum score of 1 +}; + +const enhanceWithUserData = async (contributors) => { + const enhanced = []; + + for (const contributor of contributors) { + console.log(`Fetching details for ${contributor.login}...`); + const userDetails = await fetchUserDetails(contributor.login); + + // Calculate the simplified impact score + const impactScore = calculateContributorScore(contributor); + + enhanced.push({ + id: contributor.id, + login: contributor.login, + name: userDetails?.name || contributor.login, + avatar_url: contributor.avatar_url, + html_url: contributor.html_url, + bio: userDetails?.bio || null, + company: userDetails?.company || null, + location: userDetails?.location || null, + blog: userDetails?.blog || null, + hireable: userDetails?.hireable || false, + public_repos: userDetails?.public_repos || 0, + followers: userDetails?.followers || 0, + following: userDetails?.following || 0, + created_at: userDetails?.created_at || null, + contributions: contributor.contributions, + repositories: contributor.repositories, + repo_count: contributor.repositories.length, + // Simplified scoring metrics + impact_score: impactScore, + total_prs: contributor.totalPrs, + total_merged_prs: contributor.totalMergedPrs, + total_recent_commits: contributor.totalRecentCommits, + total_reviews: contributor.totalReviews, + last_activity: contributor.lastActivity, + pr_success_rate: contributor.totalPrs > 0 ? + Math.round((contributor.totalMergedPrs / contributor.totalPrs) * 100) : 0 + }); + + // Rate limiting: delay between requests + await new Promise(resolve => setTimeout(resolve, 100)); + } + + // Sort by impact score instead of raw contributions + return enhanced.sort((a, b) => b.impact_score - a.impact_score); +}; + +// Fetch all contributors from all repositories +const fetchAllContributors = async (repositories) => { + console.log('πŸ“Š Fetching basic contributor data...'); + const contributorPromises = repositories.map(repo => + fetchContributorsForRepo(repo.name) + ); + const contributorsByRepo = await Promise.all(contributorPromises); + + // Get unique contributors to fetch detailed activity data + const uniqueContributors = new Map(); + contributorsByRepo.flat().forEach(contributor => { + if (contributor.type !== 'Bot') { + uniqueContributors.set(contributor.login, contributor); + } + }); + + console.log(`Found ${uniqueContributors.size} unique contributors across all repositories`); + return contributorsByRepo; +}; + +// Calculate scores for all contributors +const calculateAllScores = async (contributorsByRepo, repositories) => { + console.log(`πŸ“ˆ Fetching detailed activity data and calculating scores...`); + + // Fetch detailed activity data for each contributor in each repo they contribute to + const contributorsWithActivity = []; + let processedCount = 0; + + // Group contributors by repo for processing + const contributorsByRepoMap = contributorsByRepo.reduce((acc, repoContributors, idx) => { + acc[repositories[idx].name] = repoContributors.filter(c => c.type !== 'Bot'); + return acc; + }, {}); + + // Calculate total operations for progress tracking + const totalOperations = Object.values(contributorsByRepoMap).flat().length; + + for (const [repoName, contributors] of Object.entries(contributorsByRepoMap)) { + for (const contributor of contributors) { + console.log(` πŸ“Š ${++processedCount}/${totalOperations} - Fetching activity for ${contributor.login} in ${repoName}...`); + + const activityData = await fetchUserPRsAndActivity(contributor.login, repoName); + contributorsWithActivity.push({ + ...contributor, + activityData + }); + + // Rate limiting: delay between requests + await new Promise(resolve => setTimeout(resolve, 200)); + } + } + + // Aggregate contributors across all repos with activity data + const aggregatedContributors = aggregateContributors(contributorsWithActivity); + console.log(`Calculated scores for ${aggregatedContributors.length} unique contributors`); + + return aggregatedContributors; +}; + +const main = async () => { + try { + console.log('πŸš€ Starting contributor data fetch with simplified scoring...'); + + /* + * 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 + */ + + // Step 1: Fetch all repositories + const repositories = await fetchAllRepositories(); + console.log(`Found ${repositories.length} active repositories`); + + // Step 2: Fetch all contributors from all repositories + console.log('πŸ“Š Fetching all contributors from all repositories...'); + const contributorsByRepo = await fetchAllContributors(repositories); + + // Step 3: Calculate scores for all contributors + console.log('πŸ”’ Calculating contributor scores...'); + const scoredContributors = await calculateAllScores(contributorsByRepo, repositories); + + // Step 4: Enhance with detailed user information + const enhancedContributors = await enhanceWithUserData(scoredContributors); + + // Step 5: Generate output file + console.log('πŸ“ Generating output file...'); + + // Create output directory if it doesn't exist + const outputDir = path.dirname(OUTPUT_FILE); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + // Calculate summary statistics + 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); + + // Write to file + fs.writeFileSync(OUTPUT_FILE, JSON.stringify({ + generated_at: new Date().toISOString(), + total_contributors: enhancedContributors.length, + total_contributions: totalContributions, + total_impact_score: totalImpactScore, + total_recent_activity: totalRecentActivity, + scoring_method: 'simplified_recent_activity', + contributors: enhancedContributors + }, null, 2)); + + console.log(`βœ… Successfully wrote contributor data to ${OUTPUT_FILE}`); + console.log(`πŸ“Š Stats: ${enhancedContributors.length} contributors`); + console.log(` πŸ’« Total impact score: ${totalImpactScore}`); + console.log(` πŸ“ˆ Total contributions: ${totalContributions}`); + console.log(` πŸ”₯ Recent activity: ${totalRecentActivity} commits (last 6 months)`); + console.log(` πŸ† Top contributor: ${enhancedContributors[0]?.name} (${enhancedContributors[0]?.impact_score} impact score)`); + + } catch (error) { + console.error('❌ Error fetching contributors:', error); + process.exit(1); + } +}; + +if (require.main === module) { + main(); +} + +module.exports = { main }; diff --git a/src/data/contributors-manual.json b/src/data/contributors-manual.json new file mode 100644 index 0000000..ff4c6b8 --- /dev/null +++ b/src/data/contributors-manual.json @@ -0,0 +1,38 @@ +{ + "generated_at": "2025-09-03T13:46:04.752Z", + "total_contributors": 1, + "total_contributions": 24, + "total_impact_score": 24, + "total_recent_activity": 24, + "scoring_method": "manual_entry", + "contributors": [ + { + "id": 999999999, + "login": "Robobc", + "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", + "company": "Amazon Web Services", + "location": "ZΓΌrich", + "blog": null, + "hireable": null, + "public_repos": 6, + "followers": 2, + "following": 7, + "created_at": null, + "contributions": 24, + "repositories": [ + "Transform-your-APIs-into-agent-tools-with-UTCP" + ], + "repo_count": 1, + "impact_score": 24, + "total_prs": 0, + "total_merged_prs": 0, + "total_recent_commits": 24, + "total_reviews": 0, + "last_activity": "2025-09-03T13:46:04Z", + "pr_success_rate": 0 + } + ] +} diff --git a/src/data/contributors.json b/src/data/contributors.json new file mode 100644 index 0000000..47b7026 --- /dev/null +++ b/src/data/contributors.json @@ -0,0 +1,307 @@ +{ + "generated_at": "2025-09-03T14:53:34.638Z", + "total_contributors": 10, + "total_contributions": 505, + "total_impact_score": 359, + "total_recent_activity": 361, + "scoring_method": "simplified_recent_activity", + "contributors": [ + { + "id": 43811028, + "login": "h3xxit", + "name": "Razvan Radulescu", + "avatar_url": "https://avatars.githubusercontent.com/u/43811028?v=4", + "html_url": "https://github.com/h3xxit", + "bio": null, + "company": null, + "location": null, + "blog": null, + "hireable": false, + "public_repos": 18, + "followers": 22, + "following": 2, + "created_at": "2018-10-03T10:10:15Z", + "contributions": 169, + "repositories": [ + "python-utcp", + "utcp-specification", + "typescript-utcp", + "go-utcp", + "utcp-mcp", + "agent-implementation-example", + "utcp-examples", + "langchain-utcp-adapters", + "utcp-agent" + ], + "repo_count": 9, + "impact_score": 169, + "total_prs": 165, + "total_merged_prs": 133, + "total_recent_commits": 169, + "total_reviews": 9, + "last_activity": "2025-09-02T14:45:10Z", + "pr_success_rate": 81 + }, + { + "id": 170965471, + "login": "Raezil", + "name": "Kamil Mosciszko", + "avatar_url": "https://avatars.githubusercontent.com/u/170965471?v=4", + "html_url": "https://github.com/Raezil", + "bio": null, + "company": null, + "location": null, + "blog": null, + "hireable": true, + "public_repos": 28, + "followers": 30, + "following": 59, + "created_at": "2024-05-27T16:22:55Z", + "contributions": 244, + "repositories": [ + "go-utcp" + ], + "repo_count": 1, + "impact_score": 100, + "total_prs": 86, + "total_merged_prs": 64, + "total_recent_commits": 100, + "total_reviews": 3, + "last_activity": "2025-08-29T12:15:29Z", + "pr_success_rate": 74 + }, + { + "id": 69489757, + "login": "edujuan", + "name": "Juan V.", + "avatar_url": "https://avatars.githubusercontent.com/u/69489757?v=4", + "html_url": "https://github.com/edujuan", + "bio": null, + "company": null, + "location": null, + "blog": null, + "hireable": false, + "public_repos": 17, + "followers": 3, + "following": 3, + "created_at": "2020-08-11T00:28:15Z", + "contributions": 38, + "repositories": [ + "python-utcp", + "utcp-specification", + ".github", + "utcp-mcp" + ], + "repo_count": 4, + "impact_score": 38, + "total_prs": 67, + "total_merged_prs": 57, + "total_recent_commits": 38, + "total_reviews": 0, + "last_activity": "2025-08-21T12:32:23Z", + "pr_success_rate": 85 + }, + { + "id": 5594869, + "login": "perrozzi", + "name": "perrozzi", + "avatar_url": "https://avatars.githubusercontent.com/u/5594869?v=4", + "html_url": "https://github.com/perrozzi", + "bio": null, + "company": null, + "location": null, + "blog": null, + "hireable": false, + "public_repos": 65, + "followers": 5, + "following": 1, + "created_at": "2013-10-02T14:02:46Z", + "contributions": 24, + "repositories": [ + "python-utcp", + "utcp-mcp", + "utcp-examples", + "langchain-utcp-adapters" + ], + "repo_count": 4, + "impact_score": 24, + "total_prs": 54, + "total_merged_prs": 44, + "total_recent_commits": 24, + "total_reviews": 0, + "last_activity": "2025-09-02T14:17:44Z", + "pr_success_rate": 81 + }, + { + "id": 7006124, + "login": "PylotLight", + "name": "Light", + "avatar_url": "https://avatars.githubusercontent.com/u/7006124?v=4", + "html_url": "https://github.com/PylotLight", + "bio": "Devops+Dev+Golang \r\nSome projects done in: C#, blazor, Python, bash and Powershell.", + "company": null, + "location": null, + "blog": null, + "hireable": false, + "public_repos": 26, + "followers": 6, + "following": 0, + "created_at": "2014-03-20T02:28:28Z", + "contributions": 11, + "repositories": [ + "typescript-utcp-sdk" + ], + "repo_count": 1, + "impact_score": 11, + "total_prs": 0, + "total_merged_prs": 0, + "total_recent_commits": 11, + "total_reviews": 0, + "last_activity": "2025-08-31T02:56:43Z", + "pr_success_rate": 0 + }, + { + "id": 21357398, + "login": "AndreiGS", + "name": "Andrei Ghiurtu", + "avatar_url": "https://avatars.githubusercontent.com/u/21357398?v=4", + "html_url": "https://github.com/AndreiGS", + "bio": null, + "company": "Tremend Software Consulting SRL", + "location": "Brasov, Romania", + "blog": null, + "hireable": false, + "public_repos": 18, + "followers": 2, + "following": 1, + "created_at": "2016-08-31T08:24:11Z", + "contributions": 7, + "repositories": [ + "python-utcp", + "typescript-utcp", + "utcp-examples" + ], + "repo_count": 3, + "impact_score": 7, + "total_prs": 51, + "total_merged_prs": 43, + "total_recent_commits": 7, + "total_reviews": 5, + "last_activity": "2025-09-02T14:17:22Z", + "pr_success_rate": 84 + }, + { + "id": 46451363, + "login": "ulughbeck", + "name": "Ulugbek", + "avatar_url": "https://avatars.githubusercontent.com/u/46451363?v=4", + "html_url": "https://github.com/ulughbeck", + "bio": null, + "company": null, + "location": null, + "blog": null, + "hireable": false, + "public_repos": 13, + "followers": 15, + "following": 13, + "created_at": "2019-01-07T15:20:48Z", + "contributions": 6, + "repositories": [ + "python-utcp", + "utcp-mcp" + ], + "repo_count": 2, + "impact_score": 5, + "total_prs": 48, + "total_merged_prs": 38, + "total_recent_commits": 6, + "total_reviews": 0, + "last_activity": "2025-07-31T09:55:48Z", + "pr_success_rate": 79 + }, + { + "id": 150209392, + "login": "bruce-gene", + "name": "zero", + "avatar_url": "https://avatars.githubusercontent.com/u/150209392?v=4", + "html_url": "https://github.com/bruce-gene", + "bio": null, + "company": null, + "location": null, + "blog": null, + "hireable": false, + "public_repos": 17, + "followers": 2, + "following": 4, + "created_at": "2023-11-08T02:28:44Z", + "contributions": 4, + "repositories": [ + "utcp-specification" + ], + "repo_count": 1, + "impact_score": 3, + "total_prs": 19, + "total_merged_prs": 19, + "total_recent_commits": 4, + "total_reviews": 0, + "last_activity": "2025-08-01T15:02:36Z", + "pr_success_rate": 100 + }, + { + "id": 210855846, + "login": "aliraza1006", + "name": "Ali", + "avatar_url": "https://avatars.githubusercontent.com/u/210855846?v=4", + "html_url": "https://github.com/aliraza1006", + "bio": null, + "company": null, + "location": null, + "blog": null, + "hireable": false, + "public_repos": 0, + "followers": 2, + "following": 8, + "created_at": "2025-05-08T12:17:27Z", + "contributions": 1, + "repositories": [ + ".github" + ], + "repo_count": 1, + "impact_score": 1, + "total_prs": 0, + "total_merged_prs": 0, + "total_recent_commits": 1, + "total_reviews": 0, + "last_activity": "2025-07-11T12:42:11Z", + "pr_success_rate": 0 + }, + { + "id": 89096740, + "login": "SneyX", + "name": "Tiago Prelato", + "avatar_url": "https://avatars.githubusercontent.com/u/89096740?v=4", + "html_url": "https://github.com/SneyX", + "bio": "Passionate full-stack developer.", + "company": null, + "location": "Argentina", + "blog": null, + "hireable": false, + "public_repos": 23, + "followers": 1, + "following": 4, + "created_at": "2021-08-17T17:07:58Z", + "contributions": 1, + "repositories": [ + "utcp-examples" + ], + "repo_count": 1, + "impact_score": 1, + "total_prs": 6, + "total_merged_prs": 6, + "total_recent_commits": 1, + "total_reviews": 0, + "last_activity": "2025-07-24T16:29:37Z", + "pr_success_rate": 100 + } + ] +} \ No newline at end of file diff --git a/src/pages/hall-of-fame.module.css b/src/pages/hall-of-fame.module.css new file mode 100644 index 0000000..b931169 --- /dev/null +++ b/src/pages/hall-of-fame.module.css @@ -0,0 +1,547 @@ +.contributorsContainer { + min-height: 100vh; + background-color: #000000; + color: #ffffff; +} + +.header { + text-align: center; + padding: 4rem 1rem 3rem; + background: linear-gradient(135deg, #000000 0%, #111111 50%, #000000 100%); + border-bottom: 1px solid #333333; + position: relative; +} + +.header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent 0%, #8b5cf6 50%, transparent 100%); +} + +.headerContent { + max-width: 800px; + margin: 0 auto; +} + +.title { + font-size: 3.5rem; + font-weight: 800; + margin: 0 0 1rem 0; + background: linear-gradient(135deg, #ffffff 0%, #8b5cf6 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0 4px 8px rgba(139, 92, 246, 0.3); +} + +.subtitle { + font-size: 1.25rem; + color: #cccccc; + margin: 0 0 2rem 0; + line-height: 1.5; + max-width: 600px; + margin-left: auto; + margin-right: auto; +} + +.stats { + display: flex; + justify-content: center; + gap: 2rem; + margin-top: 2rem; +} + +.statBadge { + background: linear-gradient(145deg, #1a1a1a 0%, #111111 100%); + border: 1px solid #8b5cf6; + border-radius: 12px; + padding: 1rem 1.5rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + box-shadow: 0 4px 16px rgba(139, 92, 246, 0.1); + transition: all 0.3s ease; +} + +.statBadge:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(139, 92, 246, 0.2); +} + +.statValue { + font-size: 2rem; + font-weight: 700; + color: #8b5cf6; + line-height: 1; +} + +.statUnit { + font-size: 0.65rem; + color: #888888; + text-transform: uppercase; + letter-spacing: 0.3px; +} + +.leaderboard { + padding: 4rem 1rem; + max-width: 1200px; + margin: 0 auto; +} + +.leaderboardHeader { + text-align: center; + margin-bottom: 3rem; +} + +.leaderboardTitle { + font-size: 2.5rem; + font-weight: 700; + margin: 0 0 0.5rem 0; + color: #ffffff; +} + +.leaderboardSubtitle { + font-size: 1rem; + color: #888888; + margin: 0; +} + +/* Contributors List - Product Hunt Style */ +.contributorsList { + display: flex; + flex-direction: column; + gap: 0; + max-width: 100%; +} + +.contributorRow { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.5rem; + background: linear-gradient(145deg, #111111 0%, #0a0a0a 100%); + border-bottom: 1px solid #222222; + transition: all 0.2s ease; + position: relative; +} + +.contributorRow:first-child { + border-top-left-radius: 12px; + border-top-right-radius: 12px; +} + +.contributorRow:last-child { + border-bottom-left-radius: 12px; + border-bottom-right-radius: 12px; + border-bottom: none; +} + +.contributorRow:hover { + background: linear-gradient(145deg, #1a1a1a 0%, #111111 100%); + transform: translateX(4px); + box-shadow: 0 2px 12px rgba(139, 92, 246, 0.15); +} + +.contributorRow::before { + content: ''; + position: absolute; + left: 0; + top: 0; + bottom: 0; + width: 3px; + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); + opacity: 0; + transition: opacity 0.2s ease; +} + +.contributorRow:hover::before { + opacity: 1; +} + +.rowLeft { + display: flex; + align-items: center; + gap: 1rem; + flex: 1; + min-width: 0; +} + +.rowRight { + display: flex; + align-items: center; + gap: 1rem; + flex-shrink: 0; +} + +.avatar { + font-size: 2.5rem; + line-height: 1; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + width: 50px; + height: 50px; + background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%); + border-radius: 8px; + border: 1px solid #333333; +} + +.contributorInfo { + flex: 1; + min-width: 0; +} + +.contributorName { + font-size: 1.125rem; + font-weight: 600; + margin: 0 0 0.25rem 0; + color: #ffffff; + line-height: 1.2; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.rankNumber { + color: #ffffff; + font-weight: 700; + flex-shrink: 0; + transition: color 0.2s ease; +} + +.nameLink { + color: #ffffff; + text-decoration: none; + transition: color 0.2s ease; +} + +.contributorName:hover .rankNumber, +.contributorName:hover .nameLink { + color: #8b5cf6 !important; +} + +.contributorDescription { + font-size: 0.9rem; + color: #cccccc; + margin: 0 0 0.25rem 0; + line-height: 1.3; +} + +.contributorStatus { + font-size: 0.75rem; + color: #666666; + margin: 0; + font-style: italic; + opacity: 0.8; +} + +.jobButton { + background: transparent; + color: #22c55e; + border: 1px solid #22c55e; + border-radius: 6px; + padding: 0.4rem 0.8rem; + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 0.4rem; + white-space: nowrap; +} + +.jobButton:hover { + background: rgba(34, 197, 94, 0.1); + border-color: #16a34a; +} + +.badges { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.badge { + background: linear-gradient(135deg, #fbbf24 0%, #f59e0b 100%); + border-radius: 50%; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + cursor: help; + transition: all 0.2s ease; + box-shadow: 0 2px 6px rgba(251, 191, 36, 0.3); +} + +.badge:hover { + transform: translateY(-1px) scale(1.05); + box-shadow: 0 4px 12px rgba(251, 191, 36, 0.4); +} + +.badgeIcon { + font-size: 1rem; + line-height: 1; +} + +.contributionCount { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + padding: 0.5rem 1rem; + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); + border-radius: 8px; + min-width: 60px; + text-align: center; + box-shadow: 0 2px 8px rgba(139, 92, 246, 0.3); +} + +.countNumber { + font-size: 1.5rem; + font-weight: 700; + color: #ffffff; + line-height: 1; +} + +.contributionCount::after { + content: 'contribution score'; + font-size: 0.5rem; + color: rgba(255, 255, 255, 0.7); + text-transform: uppercase; + letter-spacing: 0.2px; +} + +.callToAction { + background: linear-gradient(135deg, #0a0a0a 0%, #111111 50%, #0a0a0a 100%); + padding: 4rem 1rem; + text-align: center; + border-top: 1px solid #333333; + position: relative; +} + +.callToAction::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 2px; + background: linear-gradient(90deg, transparent 0%, #8b5cf6 50%, transparent 100%); +} + +.ctaContent { + max-width: 600px; + margin: 0 auto; +} + +.ctaTitle { + font-size: 2.5rem; + font-weight: 700; + margin: 0 0 1rem 0; + color: #ffffff; +} + +.ctaText { + font-size: 1.1rem; + color: #cccccc; + margin: 0 0 2rem 0; + line-height: 1.6; +} + +.ctaButtons { + display: flex; + justify-content: center; + gap: 1rem; + flex-wrap: wrap; +} + +.ctaButton { + background: linear-gradient(135deg, #8b5cf6 0%, #7c3aed 100%); + color: #ffffff; + padding: 1rem 2rem; + border-radius: 12px; + font-size: 1rem; + font-weight: 600; + text-decoration: none; + transition: all 0.3s ease; + box-shadow: 0 4px 16px rgba(139, 92, 246, 0.3); + border: none; + cursor: pointer; + display: inline-flex; + align-items: center; + white-space: nowrap; +} + +.ctaButton:hover { + background: linear-gradient(135deg, #a78bfa 0%, #8b5cf6 100%); + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(139, 92, 246, 0.4); + color: #ffffff; + text-decoration: none; +} + +.ctaButtonSecondary { + background: transparent; + color: #8b5cf6; + padding: 1rem 2rem; + border: 2px solid #8b5cf6; + border-radius: 12px; + font-size: 1rem; + font-weight: 600; + text-decoration: none; + transition: all 0.3s ease; + display: inline-flex; + align-items: center; + white-space: nowrap; +} + +.ctaButtonSecondary:hover { + background: #8b5cf6; + color: #ffffff; + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(139, 92, 246, 0.3); + text-decoration: none; +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .title { + font-size: 2.5rem; + } + + .stats { + flex-wrap: wrap; + gap: 1rem; + justify-content: center; + } + + .statBadge { + min-width: 200px; + } + + .contributorRow { + padding: 1.25rem 1rem; + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .rowLeft { + width: 100%; + } + + .rowRight { + width: 100%; + justify-content: space-between; + flex-wrap: wrap; + } + + .jobButton { + font-size: 0.7rem; + padding: 0.3rem 0.6rem; + } + + .badges { + gap: 0.4rem; + } + + .badge { + width: 28px; + height: 28px; + } + + .badgeIcon { + font-size: 0.9rem; + } + + .avatar { + font-size: 2rem; + width: 40px; + height: 40px; + } + + .contributorName { + font-size: 1rem; + gap: 0.4rem; + } + + .rankNumber { + font-size: 0.9rem; + } + + .contributorDescription { + font-size: 0.85rem; + } + + .contributorStatus { + font-size: 0.7rem; + } + + .contributionCount { + padding: 0.75rem 1rem; + min-width: 80px; + } + + .countNumber { + font-size: 1.25rem; + } + + .ctaButtons { + flex-direction: column; + align-items: center; + } + + .ctaButton, + .ctaButtonSecondary { + width: 100%; + max-width: 300px; + justify-content: center; + } +} + +@media (max-width: 480px) { + .header { + padding: 3rem 1rem 2rem; + } + + .title { + font-size: 2rem; + } + + .subtitle { + font-size: 1rem; + } + + .leaderboard { + padding: 3rem 1rem; + } + + .leaderboardTitle { + font-size: 2rem; + } + + .contributorRow { + padding: 1rem 0.75rem; + } + + .contributorRow:hover { + transform: none; + } + + .ctaTitle { + font-size: 2rem; + } + + .callToAction { + padding: 3rem 1rem; + } +} \ No newline at end of file diff --git a/src/pages/hall-of-fame.tsx b/src/pages/hall-of-fame.tsx new file mode 100644 index 0000000..c04f6d6 --- /dev/null +++ b/src/pages/hall-of-fame.tsx @@ -0,0 +1,395 @@ +import React, { useState, useEffect } 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 +const Tooltip: React.FC<{ children: React.ReactNode; text: string }> = ({ children, text }) => { + const [isVisible, setIsVisible] = useState(false); + + return ( +
setIsVisible(true)} + onMouseLeave={() => setIsVisible(false)} + > + {children} + {isVisible && ( +
+ {text} +
+
+ )} +
+ ); +}; + +// Utility function to format date +const formatDate = (dateString: string | null): string => { + if (!dateString) return 'Unknown'; + const date = new Date(dateString); + return date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }); +}; + +// 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)) { + return 'Founding Contributor'; + } + + if (username === 'Raezil') { + return 'Lead Developer'; + } + + const now = new Date(); + const lastActivityDate = lastActivity ? new Date(lastActivity) : null; + const daysSinceLastActivity = lastActivityDate ? + (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 >= 50) { + return daysSinceLastActivity <= 90 ? 'Active contributor' : 'Regular contributor'; + } + if (contributionScore >= 20) return 'Regular contributor'; + return 'New contributor'; +}; + +// Function to generate fallback avatar emoji based on username (if image fails to load) +const generateFallbackAvatar = (username: string): string => { + const avatars = ['πŸš€', '⚑', 'πŸ”’', 'πŸ“', '🎨', 'πŸ› οΈ', '☁️', '🌟', 'πŸ”₯', 'πŸ’Ž', '🌊', 'πŸŒ™', 'β˜€οΈ', '🌈', 'πŸ¦„']; + const hash = username.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); + return avatars[hash % avatars.length]; +}; + +// Function to determine primary role based on repositories +const getPrimaryRole = (repositories: string[], contributionScore: number, username: string): string => { + if (repositories.length === 0) return 'Contributor'; + + // Special roles for UTCP admins + const utcpAdmins = ['h3xxit', 'aliraza1006', 'edujuan', 'ulughbeck', 'AndreiGS']; + if (utcpAdmins.includes(username)) { + return 'UTCP Admin'; + } + + // Special role for Kamil + if (username === 'Raezil') { + return 'Lead Maintainer of Go Port'; + } + + + // Map common repository patterns to roles + const rolePatterns = [ + { pattern: /typescript|ts-sdk|js-sdk|javascript/i, role: 'TypeScript/JavaScript SDK' }, + { pattern: /python|py-sdk/i, role: 'Python SDK' }, + { pattern: /go-sdk|golang/i, role: 'Go SDK' }, + { pattern: /rust-sdk|rs-sdk/i, role: 'Rust SDK' }, + { pattern: /java-sdk/i, role: 'Java SDK' }, + { pattern: /csharp|cs-sdk|dotnet/i, role: 'C# SDK' }, + { pattern: /php-sdk/i, role: 'PHP SDK' }, + { pattern: /specification|spec|docs/i, role: 'Specification & Docs' }, + { pattern: /examples|samples/i, role: 'Examples & Samples' }, + { pattern: /tools|utilities/i, role: 'Tools & Utilities' } + ]; + + for (const { pattern, role } of rolePatterns) { + if (repositories.some(repo => pattern.test(repo))) { + return role; + } + } + + return repositories.length > 1 ? 'Multi-language SDK' : 'Contributor'; +}; + +// 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); + + // Determine top contribution based on repositories + const topContribution = ghContributor.repo_count > 1 + ? `Cross-project development (${ghContributor.repo_count} repos)` + : ghContributor.repositories[0] || 'Code contributions'; + + // Format recent activity + const recentActivity = ghContributor.total_recent_commits > 0 + ? `${ghContributor.total_recent_commits} recent commits` + : 'No recent activity'; + + // Format quality metrics + 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 + ].filter(Boolean).join(' β€’ ') || 'Building great code'; + + // Add star badge for high contributors + const badges: Array<{ icon: string; tooltip: string }> = []; + if (ghContributor.impact_score >= 100) { + badges.push({ icon: "⭐", tooltip: "High Contributor" }); + } + + return { + id: ghContributor.id, + name: ghContributor.name || ghContributor.login, + username: ghContributor.login, + githubUsername: ghContributor.login, + role, + status, + contributions: ghContributor.contributions, + impact_score: ghContributor.impact_score, + avatar, + joinDate, + topContribution, + recentActivity, + qualityMetrics, + lookingForJob: ghContributor.hireable, + total_recent_commits: ghContributor.total_recent_commits, + badges: badges.length > 0 ? badges : undefined + }; +}; + +// Fallback data in case GitHub data isn't available +const fallbackContributors: DisplayContributor[] = [ + { + id: 1, + name: "Loading contributors...", + username: "loading", + githubUsername: "loading", + role: "Fetching from GitHub", + status: "Loading", + contributions: 0, + impact_score: 0, + avatar: "⏳", + joinDate: "Loading...", + topContribution: "Loading...", + recentActivity: "Loading...", + qualityMetrics: "Loading...", + total_recent_commits: 0, + lookingForJob: false + } +]; + +// Custom hook to load contributors data +const useContributors = (): DisplayContributor[] => { + const [contributors, setContributors] = useState(fallbackContributors); + + useEffect(() => { + const loadContributors = async () => { + let allContributors: GitHubContributor[] = []; + + // Load automatically generated contributors + try { + const autoModule = await import('../data/contributors.json'); + const autoContributorsData = autoModule.default as ContributorsData; + allContributors = [...allContributors, ...autoContributorsData.contributors]; + } catch (error) { + console.warn('Could not load auto-generated contributors data from GitHub.', 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); + } + + // 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 + setContributors(transformedContributors); + } else { + // No contributors loaded, show fallback message + setContributors([{ + id: 1, + name: "Contributors data unavailable", + username: "unavailable", + githubUsername: "universal-tool-calling-protocol", + role: "Run 'npm run fetch-contributors' to load data", + status: "Data unavailable", + contributions: 0, + impact_score: 0, + avatar: "⚠️", + joinDate: "N/A", + topContribution: "GitHub API data needed", + recentActivity: "N/A", + qualityMetrics: "N/A", + total_recent_commits: 0, + lookingForJob: false + }]); + } + }; + + loadContributors(); + }, []); + + 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} +

+
+
+ +
+ {contributor.lookingForJob && ( + + )} + {contributor.badges && ( +
+ {contributor.badges.map((badge, index) => ( +
+ {badge.icon} +
+ ))} +
+ )} +
+ + {contributor.impact_score} + +
+
+
+); + +export default function Contributors(): React.ReactNode { + const contributors = useContributors(); + + return ( + +
+
+
+

Hall-of-Fame

+

+ Meet the amazing people building the Universal Tool Calling Protocol. + We are extremely grateful for each and every one of you! +

+

+ Ranked by contribution score: recent activity across all repositories +

+
+
+ +
+
+ {contributors.map((contributor, index) => ( + + ))} +
+
+ +
+
+

Want to contribute?

+

+ Join our community of developers building the future of tool calling protocols. +

+ +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/types/contributors.ts b/src/types/contributors.ts new file mode 100644 index 0000000..a211700 --- /dev/null +++ b/src/types/contributors.ts @@ -0,0 +1,60 @@ +export interface GitHubContributor { + id: number; + login: string; + name: string; + avatar_url: string; + html_url: string; + bio: string | null; + company: string | null; + location: string | null; + blog: string | null; + hireable: boolean; + public_repos: number; + followers: number; + following: number; + created_at: string | null; + contributions: number; + repositories: string[]; + repo_count: number; + // Simplified scoring fields + impact_score: number; + total_prs: number; + total_merged_prs: number; + total_recent_commits: number; + total_reviews: number; + last_activity: string | null; + pr_success_rate: number; +} + +export interface ContributorsData { + generated_at: string; + total_contributors: number; + total_contributions: number; + total_impact_score: number; + total_recent_activity: number; + scoring_method: string; + contributors: GitHubContributor[]; +} + +export interface DisplayContributor { + id: number; + name: string; + username: string; + githubUsername: string; + role: string; + status: string; + contributions: number; + impact_score: number; + avatar: string; // GitHub avatar URL or fallback emoji + joinDate: string; + topContribution: string; + lookingForJob: boolean; + // Additional scoring display data + recentActivity?: string; + qualityMetrics?: string; + total_recent_commits: number; + badges?: Array<{ + icon: string; + tooltip: string; + }>; +} diff --git a/src/types/json-modules.d.ts b/src/types/json-modules.d.ts new file mode 100644 index 0000000..f3f5966 --- /dev/null +++ b/src/types/json-modules.d.ts @@ -0,0 +1,4 @@ +declare module "*.json" { + const value: any; + export default value; +} From 7e46f3e7f3a837955fbc1e27527219f2842efdc0 Mon Sep 17 00:00:00 2001 From: "Juan V." Date: Wed, 3 Sep 2025 17:16:44 +0200 Subject: [PATCH 3/9] hall-of-fame v2 --- .github/workflows/update-contributors.yml | 2 +- CONTRIBUTORS.md => hall-of-fame-docs.md | 0 package.json | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename CONTRIBUTORS.md => hall-of-fame-docs.md (100%) diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml index ec67a3c..2a580ec 100644 --- a/.github/workflows/update-contributors.yml +++ b/.github/workflows/update-contributors.yml @@ -34,7 +34,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | echo "πŸš€ Starting contributor data fetch..." - node scripts/fetch-contributors.js + npm run fetch-contributors echo "βœ… Contributor data fetch completed" - name: Check if contributors data changed diff --git a/CONTRIBUTORS.md b/hall-of-fame-docs.md similarity index 100% rename from CONTRIBUTORS.md rename to hall-of-fame-docs.md diff --git a/package.json b/package.json index 73ada4b..1ab6c9f 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "docusaurus": "docusaurus", "start": "docusaurus start", - "build": "node scripts/fetch-contributors.js && docusaurus build", + "build": "docusaurus build", "build:fast": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", From c37ce14056951540fdb15b033aa48a902ceff257 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:23:27 +0200 Subject: [PATCH 4/9] Update about-us.md --- about/about-us.md | 1 + 1 file changed, 1 insertion(+) diff --git a/about/about-us.md b/about/about-us.md index 74186be..149cb49 100644 --- a/about/about-us.md +++ b/about/about-us.md @@ -39,6 +39,7 @@ The UTCP project is made possible by the dedication and contributions of our ama ### Contributors - Tiago Prelato ([LinkedIn](https://www.linkedin.com/in/tiago-prelato-257787210/), [X](https://x.com/SneyX_)) - Bruce Miao +- Lochy W ## Join Us From 7c7cb8f15cda70001899a1d95340fd878fee50c5 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:40:56 +0200 Subject: [PATCH 5/9] Update about/about-us.md Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- about/about-us.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/about/about-us.md b/about/about-us.md index 149cb49..fdbc4f9 100644 --- a/about/about-us.md +++ b/about/about-us.md @@ -32,7 +32,7 @@ The UTCP project is made possible by the dedication and contributions of our ama ### Go Port Maintainer - Kamil MoΕ›ciszko ([LinkedIn](https://www.linkedin.com/in/kamilm97/), kmosc@protonmail.com) -### Langchain Utcp Adapters Maintainter +### LangChain UTCP Adapters Maintainers - Luca Perrozzi ([LinkedIn](https://www.linkedin.com/in/luca-perrozzi/)) - Roberto Catalano ([LinkedIn](https://www.linkedin.com/in/roberto-catalano-5b7793123/), [GitHub](https://github.com/Robobc)) From c8f221dd657f1af4b72f28fdd299469b65b6ffaa Mon Sep 17 00:00:00 2001 From: "Juan V." <69489757+edujuan@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:01:20 +0200 Subject: [PATCH 6/9] Hall of fame lines of code (#40) * made lines of code instead * Update scripts/fetch-contributors.js Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * Fix 0 id --------- Co-authored-by: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- scripts/fetch-contributors.js | 200 ++++++++++++- src/data/contributors.json | 82 +++++- src/pages/hall-of-fame.module.css | 303 +++++++++++++++++++- src/pages/hall-of-fame.tsx | 456 ++++++++++++++++++++---------- src/types/contributors.ts | 26 ++ 5 files changed, 899 insertions(+), 168 deletions(-) diff --git a/scripts/fetch-contributors.js b/scripts/fetch-contributors.js index 8478a21..7bf226b 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 < (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}`); + + 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..df7da66 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,101 @@ 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" }); } + + 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, - name: ghContributor.name || ghContributor.login, - username: ghContributor.login, - githubUsername: ghContributor.login, + id: ghContributor?.id || generateHash(login), + 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 +273,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 +299,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 +354,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 +376,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 +487,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 +502,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 0d5a7ce8742ff3aee0ee8120a818f145fc1bc893 Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:52:44 +0200 Subject: [PATCH 7/9] Update hall-of-fame-docs.md Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- hall-of-fame-docs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hall-of-fame-docs.md b/hall-of-fame-docs.md index 291b013..40761ca 100644 --- a/hall-of-fame-docs.md +++ b/hall-of-fame-docs.md @@ -182,4 +182,4 @@ If the contributors page shows "Contributors data unavailable": 4. Check console for error messages ### Build Integration -The build process automatically fetches fresh data. For faster builds during development, use `npm run build:fast` to skip the data fetch. +The build process does not fetch data automatically; run `npm run fetch-contributors` before `npm run build`. `npm run build:fast` is equivalent to `npm run build`. From c1439e77409a10a2b22b4146c2a222ce8d75960c Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:53:29 +0200 Subject: [PATCH 8/9] Update package.json Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1ab6c9f..0829088 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "typecheck": "tsc", + "typecheck": "tsc --noEmit", "fetch-contributors": "node scripts/fetch-contributors.js" }, "dependencies": { From 5b27dffa4cfebbfa6e7371f4707e0bb1693b6e4c Mon Sep 17 00:00:00 2001 From: Razvan Radulescu <43811028+h3xxit@users.noreply.github.com> Date: Thu, 4 Sep 2025 13:55:08 +0200 Subject: [PATCH 9/9] Update hall-of-fame-docs.md Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> --- hall-of-fame-docs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hall-of-fame-docs.md b/hall-of-fame-docs.md index 40761ca..4474689 100644 --- a/hall-of-fame-docs.md +++ b/hall-of-fame-docs.md @@ -67,7 +67,7 @@ The generated `src/data/contributors.json` file contains: "total_contributions": 1500, "total_impact_score": 3250, "total_recent_activity": 145, - "scoring_method": "hybrid_impact_score", + "scoring_method": "simplified_recent_activity", "contributors": [ { "id": 123456,