diff --git a/.github/workflows/update-contributors.yml b/.github/workflows/update-contributors.yml new file mode 100644 index 0000000..2a580ec --- /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..." + npm run fetch-contributors + 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/about/about-us.md b/about/about-us.md index f779d20..fdbc4f9 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/)) @@ -30,11 +32,14 @@ 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 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)) + +### Contributors - Tiago Prelato ([LinkedIn](https://www.linkedin.com/in/tiago-prelato-257787210/), [X](https://x.com/SneyX_)) - Bruce Miao +- Lochy W ## Join Us 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/hall-of-fame-docs.md b/hall-of-fame-docs.md new file mode 100644 index 0000000..4474689 --- /dev/null +++ b/hall-of-fame-docs.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": "simplified_recent_activity", + "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 does not fetch data automatically; run `npm run fetch-contributors` before `npm run build`. `npm run build:fast` is equivalent to `npm run build`. diff --git a/package.json b/package.json index eb7ec2c..0829088 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,15 @@ "docusaurus": "docusaurus", "start": "docusaurus start", "build": "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 --noEmit", + "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..7bf226b --- /dev/null +++ b/scripts/fetch-contributors.js @@ -0,0 +1,526 @@ +#!/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'); + +// 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', + '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 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 + 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 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); + + return { + 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, + // 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); + return { + prs: 0, + mergedPrs: 0, + recentCommits: 0, + totalCommits: 0, + reviews: 0, + lastActivity: null, + totalAdditions: 0, + totalDeletions: 0, + totalChanges: 0, + commitsAnalyzed: 0, + commitsSuccessfullyAnalyzed: 0 + }; + } +}; + +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.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); + 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, + totalCommits: contributor.activityData?.totalCommits || 0, + totalReviews: contributor.activityData?.reviews || 0, + 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 + }); + } + }); + + 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_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, + // 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 + 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 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. 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 + 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); + 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({ + 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', + // New aggregated line change statistics + total_additions: totalAdditions, + total_deletions: totalDeletions, + total_changes: totalChanges, + total_commits_analyzed: totalCommitsAnalyzed, + 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(` π 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) { + 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..b65ebae --- /dev/null +++ b/src/data/contributors.json @@ -0,0 +1,361 @@ +{ + "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, + "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": 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": 80, + "total_additions": 210552, + "total_deletions": 54504, + "total_changes": 265056, + "commits_analyzed": 169 + }, + { + "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_commits": 244, + "total_reviews": 3, + "last_activity": "2025-08-29T12:15:29Z", + "pr_success_rate": 74, + "total_additions": 39386, + "total_deletions": 11377, + "total_changes": 50763, + "commits_analyzed": 244 + }, + { + "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": 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": 84, + "total_additions": 12605, + "total_deletions": 5194, + "total_changes": 17799, + "commits_analyzed": 38 + }, + { + "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_commits": 24, + "total_reviews": 0, + "last_activity": "2025-09-02T14:17:44Z", + "pr_success_rate": 81, + "total_additions": 12023, + "total_deletions": 3946, + "total_changes": 15969, + "commits_analyzed": 24 + }, + { + "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_commits": 11, + "total_reviews": 0, + "last_activity": "2025-08-31T02:56:43Z", + "pr_success_rate": 0, + "total_additions": 8399, + "total_deletions": 1706, + "total_changes": 10105, + "commits_analyzed": 11 + }, + { + "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_commits": 7, + "total_reviews": 5, + "last_activity": "2025-09-02T14:17:22Z", + "pr_success_rate": 84, + "total_additions": 28358, + "total_deletions": 916, + "total_changes": 29274, + "commits_analyzed": 7 + }, + { + "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_commits": 6, + "total_reviews": 0, + "last_activity": "2025-07-31T09:55:48Z", + "pr_success_rate": 79, + "total_additions": 2282, + "total_deletions": 309, + "total_changes": 2591, + "commits_analyzed": 6 + }, + { + "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": 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": 95, + "total_additions": 7, + "total_deletions": 7, + "total_changes": 14, + "commits_analyzed": 4 + }, + { + "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_commits": 1, + "total_reviews": 0, + "last_activity": "2025-07-11T12:42:11Z", + "pr_success_rate": 0, + "total_additions": 2, + "total_deletions": 2, + "total_changes": 4, + "commits_analyzed": 1 + }, + { + "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_commits": 1, + "total_reviews": 0, + "last_activity": "2025-07-24T16:29:37Z", + "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 new file mode 100644 index 0000000..f2e79a2 --- /dev/null +++ b/src/pages/hall-of-fame.module.css @@ -0,0 +1,846 @@ +.contributorsContainer { + min-height: 100vh; + background-color: #000000; + 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; + 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; +} + +/* 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 { + 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; + } + + .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; + } + + .ctaButton, + .ctaButtonSecondary { + width: 100%; + 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) { + .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; + } + + .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 new file mode 100644 index 0000000..df7da66 --- /dev/null +++ b/src/pages/hall-of-fame.tsx @@ -0,0 +1,561 @@ +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'; + +// 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 ( +
+ Meet the amazing people building the Universal Tool Calling Protocol. + We are extremely grateful for each and every one of you! +
++ Ranked by total lines changed across all repositories +
++ Join our community of developers building the future of tool calling protocols. +
+ +