Add additional sections.

This commit is contained in:
2025-12-11 11:03:20 +00:00
parent 9ded096839
commit 8073711f4b
18 changed files with 497 additions and 82 deletions
+8 -2
View File
@@ -51,7 +51,7 @@ $ git-velocity serve --port 8080
### 🎮 Gamification Engine
- **Scoring System**: Earn points for every contribution
- **93 Achievements**: Tiered progression from "First Steps" to "Code Warrior"
- **95 Achievements**: Tiered progression from "First Steps" to "Code Warrior"
- **Leaderboards**: Compete with your team
- **Tier Progression**: Multiple tiers per achievement category
- **Activity Patterns**: Track early bird, night owl, weekend, and out-of-hours commits
@@ -223,7 +223,7 @@ jobs:
## 🏆 Achievements
Git Velocity includes **93 hardcoded achievements** across 18 categories with multiple progression tiers. Achievements cannot be modified via configuration to prevent manipulation.
Git Velocity includes **95 hardcoded achievements** across 20 categories with multiple progression tiers. Achievements cannot be modified via configuration to prevent manipulation.
### Achievement Categories
@@ -249,6 +249,8 @@ Git Velocity includes **93 hardcoded achievements** across 18 categories with mu
| **Midnight** | 5, 10, 25, 50 | Commits between midnight-4am |
| **Weekend** | 5, 10, 25, 50 | Weekend commits |
| **Out of Hours** | 10, 25, 50, 100 | Commits outside 9am-5pm |
| **Documentation** | 100, 500, 1K, 2.5K, 5K | Comment/doc lines added |
| **Comment Cleanup** | 50, 200, 500, 1K, 2.5K | Outdated comments removed |
### Example Achievements
@@ -261,6 +263,10 @@ Git Velocity includes **93 hardcoded achievements** across 18 categories with mu
| 🏢 Full Work Week | 5 consecutive weekday streak |
| 🌙 Night Owl | 50 commits after 9pm |
| ♾️ Time Bender | 100 commits outside 9am-5pm |
| 📚 Documentation Hero | Added 1000 lines of comments/docs |
| 🏛️ Code Historian | Added 5000 lines of comments/docs |
| ✂️ Comment Trimmer | Removed 50 outdated comment lines |
| 💀 Dead Code Hunter | Removed 500 outdated comment lines |
## ⚙️ Configuration
+4
View File
@@ -139,6 +139,8 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
cm.LinesDeleted += commit.Deletions
cm.MeaningfulLinesAdded += commit.MeaningfulAdditions
cm.MeaningfulLinesDeleted += commit.MeaningfulDeletions
cm.CommentLinesAdded += commit.CommentAdditions
cm.CommentLinesDeleted += commit.CommentDeletions
cm.FilesChanged += commit.FilesChanged
// Update per-repo contributor stats
@@ -148,6 +150,8 @@ func (a *Aggregator) Aggregate(data *models.RawData, dateRange *config.ParsedDat
rcm.LinesDeleted += commit.Deletions
rcm.MeaningfulLinesAdded += commit.MeaningfulAdditions
rcm.MeaningfulLinesDeleted += commit.MeaningfulDeletions
rcm.CommentLinesAdded += commit.CommentAdditions
rcm.CommentLinesDeleted += commit.CommentDeletions
rcm.FilesChanged += commit.FilesChanged
// Track activity patterns based on commit time
+14
View File
@@ -357,5 +357,19 @@ func defaultAchievements() []AchievementConfig {
{ID: "ooh-25", Name: "Flexible Schedule", Description: "25 commits outside 9am-5pm", Icon: "fa-user-clock", Condition: AchievementCondition{Type: "out_of_hours_count", Threshold: 25}},
{ID: "ooh-50", Name: "Off-Hours Hero", Description: "50 commits outside 9am-5pm", Icon: "fa-hourglass-half", Condition: AchievementCondition{Type: "out_of_hours_count", Threshold: 50}},
{ID: "ooh-100", Name: "Time Bender", Description: "100 commits outside 9am-5pm", Icon: "fa-infinity", Condition: AchievementCondition{Type: "out_of_hours_count", Threshold: 100}},
// ===== DOCUMENTATION & COMMENTS ADDED (Tiers: 100, 500, 1000, 2500, 5000) =====
{ID: "docs-100", Name: "Documenter", Description: "Added 100 lines of comments/docs", Icon: "fa-file-lines", Condition: AchievementCondition{Type: "comment_lines_added", Threshold: 100}},
{ID: "docs-500", Name: "Technical Writer", Description: "Added 500 lines of comments/docs", Icon: "fa-book", Condition: AchievementCondition{Type: "comment_lines_added", Threshold: 500}},
{ID: "docs-1000", Name: "Documentation Hero", Description: "Added 1000 lines of comments/docs", Icon: "fa-book-open", Condition: AchievementCondition{Type: "comment_lines_added", Threshold: 1000}},
{ID: "docs-2500", Name: "Knowledge Keeper", Description: "Added 2500 lines of comments/docs", Icon: "fa-scroll", Condition: AchievementCondition{Type: "comment_lines_added", Threshold: 2500}},
{ID: "docs-5000", Name: "Code Historian", Description: "Added 5000 lines of comments/docs", Icon: "fa-landmark", Condition: AchievementCondition{Type: "comment_lines_added", Threshold: 5000}},
// ===== COMMENT CLEANUP (Tiers: 50, 200, 500, 1000, 2500) =====
{ID: "docs-del-50", Name: "Comment Trimmer", Description: "Removed 50 lines of outdated comments", Icon: "fa-scissors", Condition: AchievementCondition{Type: "comment_lines_deleted", Threshold: 50}},
{ID: "docs-del-200", Name: "Cleanup Crew", Description: "Removed 200 lines of outdated comments", Icon: "fa-broom", Condition: AchievementCondition{Type: "comment_lines_deleted", Threshold: 200}},
{ID: "docs-del-500", Name: "Dead Code Hunter", Description: "Removed 500 lines of outdated comments", Icon: "fa-skull-crossbones", Condition: AchievementCondition{Type: "comment_lines_deleted", Threshold: 500}},
{ID: "docs-del-1000", Name: "Comment Surgeon", Description: "Removed 1000 lines of outdated comments", Icon: "fa-scalpel", Condition: AchievementCondition{Type: "comment_lines_deleted", Threshold: 1000}},
{ID: "docs-del-2500", Name: "Noise Eliminator", Description: "Removed 2500 lines of outdated comments", Icon: "fa-volume-xmark", Condition: AchievementCondition{Type: "comment_lines_deleted", Threshold: 2500}},
}
}
+4
View File
@@ -19,6 +19,10 @@ type Commit struct {
MeaningfulAdditions int `json:"meaningful_additions"`
MeaningfulDeletions int `json:"meaningful_deletions"`
// Comment line counts
CommentAdditions int `json:"comment_additions"`
CommentDeletions int `json:"comment_deletions"`
// Derived fields
HasTests bool `json:"has_tests"`
}
+4
View File
@@ -27,6 +27,10 @@ type ContributorMetrics struct {
MeaningfulLinesAdded int `json:"meaningful_lines_added"`
MeaningfulLinesDeleted int `json:"meaningful_lines_deleted"`
// Comment and documentation line counts
CommentLinesAdded int `json:"comment_lines_added"`
CommentLinesDeleted int `json:"comment_lines_deleted"`
// PR metrics
PRsOpened int `json:"prs_opened"`
PRsMerged int `json:"prs_merged"`
+7
View File
@@ -42,6 +42,8 @@ func (c *Calculator) Calculate(metrics *models.GlobalMetrics) *models.GlobalMetr
existing.LinesDeleted += cm.LinesDeleted
existing.MeaningfulLinesAdded += cm.MeaningfulLinesAdded
existing.MeaningfulLinesDeleted += cm.MeaningfulLinesDeleted
existing.CommentLinesAdded += cm.CommentLinesAdded
existing.CommentLinesDeleted += cm.CommentLinesDeleted
existing.PRsOpened += cm.PRsOpened
existing.PRsMerged += cm.PRsMerged
existing.ReviewsGiven += cm.ReviewsGiven
@@ -257,6 +259,11 @@ func (c *Calculator) checkAchievements(cm *models.ContributorMetrics) []string {
earned = float64(cm.OutOfHoursCount) >= ach.Condition.Threshold
case "work_week_streak":
earned = float64(cm.WorkWeekStreak) >= ach.Condition.Threshold
// Documentation & comments
case "comment_lines_added":
earned = float64(cm.CommentLinesAdded) >= ach.Condition.Threshold
case "comment_lines_deleted":
earned = float64(cm.CommentLinesDeleted) >= ach.Condition.Threshold
}
if earned {
+125
View File
@@ -878,3 +878,128 @@ func TestCalculator_MeaningfulLinesScoring(t *testing.T) {
assert.Equal(t, 50, contributor.Score.Total)
})
}
func TestCalculator_CommentLinesAchievements(t *testing.T) {
t.Parallel()
t.Run("earns documentation achievements for adding comments", func(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo",
Contributors: []models.ContributorMetrics{
{
Login: "documenter",
CommitCount: 10,
CommentLinesAdded: 1500, // Should earn docs-100, docs-500, docs-1000
CommentLinesDeleted: 100, // Should earn docs-del-50
RepositoriesContributed: []string{"owner/repo"},
},
},
},
},
}
result := calc.Calculate(metrics)
contributor := result.Repositories[0].Contributors[0]
// Should have documentation achievements
assert.Contains(t, contributor.Achievements, "docs-100", "Should earn docs-100 for 100+ comment lines")
assert.Contains(t, contributor.Achievements, "docs-500", "Should earn docs-500 for 500+ comment lines")
assert.Contains(t, contributor.Achievements, "docs-1000", "Should earn docs-1000 for 1000+ comment lines")
assert.NotContains(t, contributor.Achievements, "docs-2500", "Should not earn docs-2500 for <2500 comment lines")
// Should have comment cleanup achievement
assert.Contains(t, contributor.Achievements, "docs-del-50", "Should earn docs-del-50 for 50+ comment deletions")
assert.NotContains(t, contributor.Achievements, "docs-del-200", "Should not earn docs-del-200 for <200 deletions")
})
t.Run("earns all documentation deletion achievements", func(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo",
Contributors: []models.ContributorMetrics{
{
Login: "cleanup-expert",
CommitCount: 50,
CommentLinesAdded: 100,
CommentLinesDeleted: 3000, // Should earn all deletion tiers
RepositoriesContributed: []string{"owner/repo"},
},
},
},
},
}
result := calc.Calculate(metrics)
contributor := result.Repositories[0].Contributors[0]
// Should have all comment cleanup achievements
assert.Contains(t, contributor.Achievements, "docs-del-50")
assert.Contains(t, contributor.Achievements, "docs-del-200")
assert.Contains(t, contributor.Achievements, "docs-del-500")
assert.Contains(t, contributor.Achievements, "docs-del-1000")
assert.Contains(t, contributor.Achievements, "docs-del-2500")
})
t.Run("aggregates comment lines across multiple repositories", func(t *testing.T) {
t.Parallel()
cfg := config.DefaultConfig()
cfg.Scoring.Enabled = true
calc := NewCalculator(cfg)
metrics := &models.GlobalMetrics{
Repositories: []models.RepositoryMetrics{
{
FullName: "owner/repo1",
Contributors: []models.ContributorMetrics{
{
Login: "multi-repo-doc",
CommitCount: 5,
CommentLinesAdded: 300,
CommentLinesDeleted: 30,
RepositoriesContributed: []string{"owner/repo1"},
},
},
},
{
FullName: "owner/repo2",
Contributors: []models.ContributorMetrics{
{
Login: "multi-repo-doc",
CommitCount: 5,
CommentLinesAdded: 300,
CommentLinesDeleted: 30,
RepositoriesContributed: []string{"owner/repo2"},
},
},
},
},
}
result := calc.Calculate(metrics)
// Check leaderboard entry (aggregated)
require.Len(t, result.Leaderboard, 1)
entry := result.Leaderboard[0]
// Aggregated: 300 + 300 = 600 comment lines added, 30 + 30 = 60 deleted
assert.Contains(t, entry.Achievements, "docs-100")
assert.Contains(t, entry.Achievements, "docs-500")
assert.NotContains(t, entry.Achievements, "docs-1000", "600 < 1000")
assert.Contains(t, entry.Achievements, "docs-del-50")
assert.NotContains(t, entry.Achievements, "docs-del-200", "60 < 200")
})
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -8,9 +8,9 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script type="module" crossorigin src="./assets/index-C2QviOxm.js"></script>
<script type="module" crossorigin src="./assets/index-R3eb927Q.js"></script>
<link rel="modulepreload" crossorigin href="./assets/chart-Bcjh2pZL.js">
<link rel="stylesheet" crossorigin href="./assets/index-CmyGiR94.css">
<link rel="stylesheet" crossorigin href="./assets/index-8XjWwD9J.css">
</head>
<body class="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 font-sans transition-colors duration-300">
<div id="app"></div>
+8
View File
@@ -217,6 +217,8 @@ func (r *Repository) FetchCommits(ctx context.Context, owner, name string, since
Deletions: stats.Deletions,
MeaningfulAdditions: stats.MeaningfulAdditions,
MeaningfulDeletions: stats.MeaningfulDeletions,
CommentAdditions: stats.CommentAdditions,
CommentDeletions: stats.CommentDeletions,
FilesChanged: stats.FilesChanged,
Repository: fmt.Sprintf("%s/%s", owner, name),
URL: fmt.Sprintf("https://github.com/%s/%s/commit/%s", owner, name, c.Hash.String()),
@@ -245,6 +247,8 @@ type commitStats struct {
Deletions int
MeaningfulAdditions int
MeaningfulDeletions int
CommentAdditions int
CommentDeletions int
FilesChanged int
HasTests bool
}
@@ -327,6 +331,8 @@ func (r *Repository) getCommitStats(c *object.Commit, testPatterns []string) com
stats.Additions++
if diff.IsMeaningfulLine(line) {
stats.MeaningfulAdditions++
} else if diff.IsCommentLine(line) && !diff.IsWhitespaceLine(line) {
stats.CommentAdditions++
}
}
case 2: // Delete
@@ -334,6 +340,8 @@ func (r *Repository) getCommitStats(c *object.Commit, testPatterns []string) com
stats.Deletions++
if diff.IsMeaningfulLine(line) {
stats.MeaningfulDeletions++
} else if diff.IsCommentLine(line) && !diff.IsWhitespaceLine(line) {
stats.CommentDeletions++
}
}
}
+7 -2
View File
@@ -727,9 +727,10 @@ func convertCommit(c *github.RepositoryCommit, owner, repo string) models.Commit
}
filesChanged = len(c.Files)
// Detect if commit includes tests and calculate meaningful line counts
// Detect if commit includes tests and calculate meaningful/comment line counts
hasTests := false
var meaningfulAdditions, meaningfulDeletions int
var commentAdditions, commentDeletions int
for _, f := range c.Files {
filename := f.GetFilename()
@@ -749,12 +750,14 @@ func convertCommit(c *github.RepositoryCommit, owner, repo string) models.Commit
continue
}
// Analyze file patch to get meaningful line counts
// Analyze file patch to get meaningful and comment line counts
patch := f.GetPatch()
if patch != "" {
stats := diff.AnalyzePatch(patch)
meaningfulAdditions += stats.MeaningfulAdditions
meaningfulDeletions += stats.MeaningfulDeletions
commentAdditions += stats.CommentAdditions
commentDeletions += stats.CommentDeletions
}
}
@@ -773,6 +776,8 @@ func convertCommit(c *github.RepositoryCommit, owner, repo string) models.Commit
Deletions: deletions,
MeaningfulAdditions: meaningfulAdditions,
MeaningfulDeletions: meaningfulDeletions,
CommentAdditions: commentAdditions,
CommentDeletions: commentDeletions,
FilesChanged: filesChanged,
Repository: fmt.Sprintf("%s/%s", owner, repo),
URL: c.GetHTMLURL(),
+126 -70
View File
@@ -29,97 +29,153 @@ const getTierFromThreshold = (threshold) => {
return 1
}
// Extract threshold from achievement ID (e.g., "commit-100" -> 100)
// Extract threshold from achievement ID (e.g., "commit-100" -> 100, "docs-del-50" -> 50)
const extractThreshold = (id) => {
const match = id.match(/(\d+)$/)
if (match) return parseInt(match[1], 10)
// Special cases for non-numeric achievements
if (id === 'first-commit' || id === 'pr-opener' || id === 'reviewer') return 1
return 50 // Default for special achievements
}
// Achievement definitions matching the Go backend
// Achievement definitions matching the Go backend (internal/config/schema.go)
const achievements = {
// Commit achievements - Journey from apprentice to legend
'first-commit': { name: 'Hello World', description: 'Made your first commit', icon: 'fa-baby' },
'commit-10': { name: 'Seedling', description: 'Made 10 commits', icon: 'fa-seedling' },
'commit-25': { name: 'Momentum', description: 'Made 25 commits', icon: 'fa-wind' },
'commit-50': { name: 'Trailblazer', description: 'Made 50 commits', icon: 'fa-hiking' },
'commit-100': { name: 'Centurion', description: 'Made 100 commits', icon: 'fa-shield-halved' },
'commit-250': { name: 'Relentless', description: 'Made 250 commits', icon: 'fa-bolt-lightning' },
'commit-500': { name: 'Unstoppable', description: 'Made 500 commits', icon: 'fa-meteor' },
'commit-1000': { name: 'Grandmaster', description: 'Made 1000 commits', icon: 'fa-chess-king' },
'commit-5000': { name: 'Titan', description: 'Made 5000 commits', icon: 'fa-mountain-sun' },
'commit-10000': { name: 'Immortal', description: 'Made 10000 commits', icon: 'fa-dragon' },
'commit-25000': { name: 'Ascended', description: 'Made 25000 commits', icon: 'fa-infinity' },
// ===== COMMIT COUNT (Tiers: 1, 10, 50, 100, 500, 1000) =====
'commit-1': { name: 'First Steps', description: 'Made your first commit', icon: 'fa-baby' },
'commit-10': { name: 'Getting Started', description: 'Made 10 commits', icon: 'fa-seedling' },
'commit-50': { name: 'Contributor', description: 'Made 50 commits', icon: 'fa-code' },
'commit-100': { name: 'Committed', description: 'Made 100 commits', icon: 'fa-fire' },
'commit-500': { name: 'Code Machine', description: 'Made 500 commits', icon: 'fa-robot' },
'commit-1000': { name: 'Code Warrior', description: 'Made 1000 commits', icon: 'fa-crown' },
// PR achievements - The art of collaboration
'pr-opener': { name: 'First Blood', description: 'Opened your first pull request', icon: 'fa-flag-checkered' },
'pr-10': { name: 'Collaborator', description: 'Opened 10 pull requests', icon: 'fa-handshake' },
'pr-25': { name: 'Integrator', description: 'Opened 25 pull requests', icon: 'fa-code-branch' },
'pr-50': { name: 'Architect', description: 'Opened 50 pull requests', icon: 'fa-building' },
'pr-100': { name: 'Vanguard', description: 'Opened 100 pull requests', icon: 'fa-rocket' },
// ===== PR OPENED (Tiers: 1, 10, 25, 50, 100, 250) =====
'pr-1': { name: 'PR Pioneer', description: 'Opened your first pull request', icon: 'fa-code-pull-request' },
'pr-10': { name: 'PR Regular', description: 'Opened 10 pull requests', icon: 'fa-code-branch' },
'pr-25': { name: 'PR Pro', description: 'Opened 25 pull requests', icon: 'fa-code-compare' },
'pr-50': { name: 'Merge Master', description: 'Opened 50 pull requests', icon: 'fa-code-merge' },
'pr-100': { name: 'PR Champion', description: 'Opened 100 pull requests', icon: 'fa-trophy' },
'pr-250': { name: 'PR Legend', description: 'Opened 250 pull requests', icon: 'fa-medal' },
// Review achievements - The guardian path
'reviewer': { name: 'Watchful Eye', description: 'Reviewed your first pull request', icon: 'fa-eye' },
'reviewer-10': { name: 'Sentinel', description: 'Reviewed 10 pull requests', icon: 'fa-shield' },
'reviewer-25': { name: 'Gatekeeper', description: 'Reviewed 25 pull requests', icon: 'fa-dungeon' },
'reviewer-50': { name: 'Oracle', description: 'Reviewed 50 pull requests', icon: 'fa-hat-wizard' },
'reviewer-100': { name: 'Sage', description: 'Reviewed 100 pull requests', icon: 'fa-book-skull' },
// ===== REVIEWS (Tiers: 1, 10, 25, 50, 100, 250) =====
'review-1': { name: 'First Review', description: 'Reviewed your first pull request', icon: 'fa-magnifying-glass' },
'review-10': { name: 'Reviewer', description: 'Reviewed 10 pull requests', icon: 'fa-eye' },
'review-25': { name: 'Review Regular', description: 'Reviewed 25 pull requests', icon: 'fa-glasses' },
'review-50': { name: 'Review Expert', description: 'Reviewed 50 pull requests', icon: 'fa-user-check' },
'review-100': { name: 'Review Guru', description: 'Reviewed 100 pull requests', icon: 'fa-user-graduate' },
'review-250': { name: 'Review Master', description: 'Reviewed 250 pull requests', icon: 'fa-award' },
// Speed achievements - Time is of the essence
'speed-demon': { name: 'Lightning Rod', description: 'Average review response under 1 hour', icon: 'fa-bolt' },
'quick-responder': { name: 'Flash', description: 'Average review response under 4 hours', icon: 'fa-gauge-high' },
// ===== REVIEW COMMENTS (Tiers: 10, 50, 100, 250, 500) =====
'comment-10': { name: 'Commentator', description: 'Left 10 PR review comments', icon: 'fa-comment' },
'comment-50': { name: 'Feedback Giver', description: 'Left 50 PR review comments', icon: 'fa-comments' },
'comment-100': { name: 'Code Critic', description: 'Left 100 PR review comments', icon: 'fa-comment-dots' },
'comment-250': { name: 'Feedback Expert', description: 'Left 250 PR review comments', icon: 'fa-message' },
'comment-500': { name: 'Comment Champion', description: 'Left 500 PR review comments', icon: 'fa-scroll' },
// Comment achievements
'commentator': { name: 'Wordsmith', description: 'Left 50 PR review comments', icon: 'fa-feather-pointed' },
// ===== LINES ADDED (Tiers: 100, 1000, 5000, 10000, 50000) =====
'lines-added-100': { name: 'First Hundred', description: 'Added 100 lines of code', icon: 'fa-plus' },
'lines-added-1000': { name: 'Thousand Lines', description: 'Added 1000 lines of code', icon: 'fa-layer-group' },
'lines-added-5000': { name: 'Five Thousand', description: 'Added 5000 lines of code', icon: 'fa-cubes' },
'lines-added-10000': { name: 'Ten Thousand', description: 'Added 10000 lines of code', icon: 'fa-mountain' },
'lines-added-50000': { name: 'Code Mountain', description: 'Added 50000 lines of code', icon: 'fa-mountain-sun' },
// Lines of code achievements - Volume mastery
'lines-1000': { name: 'Scribe', description: 'Added 1000 lines of code', icon: 'fa-scroll' },
'lines-10000': { name: 'Novelist', description: 'Added 10000 lines of code', icon: 'fa-book' },
'lines-100000': { name: 'Encyclopedia', description: 'Added 100000 lines of code', icon: 'fa-landmark' },
// ===== LINES DELETED (Tiers: 100, 500, 1000, 5000, 10000) =====
'lines-deleted-100': { name: 'Tidying Up', description: 'Deleted 100 lines of code', icon: 'fa-eraser' },
'lines-deleted-500': { name: 'Spring Cleaning', description: 'Deleted 500 lines of code', icon: 'fa-broom' },
'lines-deleted-1000': { name: 'Code Cleaner', description: 'Deleted 1000 lines of code', icon: 'fa-trash-can' },
'lines-deleted-5000': { name: 'Refactoring Hero', description: 'Deleted 5000 lines of code', icon: 'fa-recycle' },
'lines-deleted-10000': { name: 'Deletion Master', description: 'Deleted 10000 lines of code', icon: 'fa-dumpster-fire' },
// Deletion achievements - The minimalist way
'cleaner': { name: 'Pruner', description: 'Deleted 1000 lines of code', icon: 'fa-scissors' },
'refactorer': { name: 'Surgeon', description: 'Deleted 10000 lines of code', icon: 'fa-syringe' },
'annihilator': { name: 'Annihilator', description: 'Deleted 100000 lines of code', icon: 'fa-explosion' },
// ===== REVIEW RESPONSE TIME (Tiers: 24h, 4h, 1h) =====
'review-time-24h': { name: 'Same Day Reviewer', description: 'Average review response under 24 hours', icon: 'fa-clock' },
'review-time-4h': { name: 'Quick Responder', description: 'Average review response under 4 hours', icon: 'fa-stopwatch' },
'review-time-1h': { name: 'Speed Demon', description: 'Average review response under 1 hour', icon: 'fa-bolt' },
// Multi-repo achievements - The wanderer
'multi-repo': { name: 'Nomad', description: 'Contributed to 5 repositories', icon: 'fa-compass' },
'multi-repo-10': { name: 'Explorer', description: 'Contributed to 10 repositories', icon: 'fa-map' },
// ===== MULTI-REPO (Tiers: 2, 5, 10) =====
'repo-2': { name: 'Multi-Repo', description: 'Contributed to 2 repositories', icon: 'fa-folder' },
'repo-5': { name: 'Repo Explorer', description: 'Contributed to 5 repositories', icon: 'fa-folder-tree' },
'repo-10': { name: 'Repo Master', description: 'Contributed to 10 repositories', icon: 'fa-network-wired' },
// Team collaboration - Social butterfly
'team-player': { name: 'Ambassador', description: 'Reviewed PRs from 10 different contributors', icon: 'fa-users' },
'team-player-25': { name: 'Diplomat', description: 'Reviewed PRs from 25 different contributors', icon: 'fa-globe' },
// ===== UNIQUE REVIEWEES (Tiers: 3, 10, 25) =====
'reviewees-3': { name: 'Helpful Colleague', description: 'Reviewed PRs from 3 different contributors', icon: 'fa-user-group' },
'reviewees-10': { name: 'Team Player', description: 'Reviewed PRs from 10 different contributors', icon: 'fa-people-group' },
'reviewees-25': { name: 'Community Pillar', description: 'Reviewed PRs from 25 different contributors', icon: 'fa-people-roof' },
// PR size achievements - Go big or go home
'big-pr': { name: 'Heavyweight', description: 'Merged a PR with 1000+ lines', icon: 'fa-dumbbell' },
'mega-pr': { name: 'Colossus', description: 'Merged a PR with 5000+ lines', icon: 'fa-monument' },
// ===== PR SIZE - LARGE (Tiers: 500, 1000, 5000) =====
'large-pr-500': { name: 'Big Change', description: 'Merged a PR with 500+ lines changed', icon: 'fa-expand' },
'large-pr-1000': { name: 'Heavy Lifter', description: 'Merged a PR with 1000+ lines changed', icon: 'fa-weight-hanging' },
'large-pr-5000': { name: 'Mega Merge', description: 'Merged a PR with 5000+ lines changed', icon: 'fa-dumbbell' },
// Small PR achievements - Precision strikes
'small-pr-10': { name: 'Minimalist', description: 'Merged 10 PRs under 100 lines', icon: 'fa-compress' },
'small-pr-50': { name: 'Atomic', description: 'Merged 50 PRs under 100 lines', icon: 'fa-atom' },
// ===== SMALL PRs (Tiers: 5, 10, 25, 50) =====
'small-pr-5': { name: 'Small Changes', description: 'Merged 5 PRs under 100 lines', icon: 'fa-compress' },
'small-pr-10': { name: 'Small PR Advocate', description: 'Merged 10 PRs under 100 lines', icon: 'fa-minimize' },
'small-pr-25': { name: 'Atomic Commits', description: 'Merged 25 PRs under 100 lines', icon: 'fa-atom' },
'small-pr-50': { name: 'Micro PR Master', description: 'Merged 50 PRs under 100 lines', icon: 'fa-microchip' },
// Perfect PR achievements - Flawless execution
'perfect-pr-5': { name: 'Sharpshooter', description: '5 PRs merged without changes requested', icon: 'fa-bullseye' },
'perfect-pr-25': { name: 'Perfectionist', description: '25 PRs merged without changes requested', icon: 'fa-gem' },
'perfect-pr-100': { name: 'Immaculate', description: '100 PRs merged without changes requested', icon: 'fa-crown' },
// ===== PERFECT PRs (Tiers: 1, 5, 10, 25) =====
'perfect-pr-1': { name: 'First Try', description: '1 PR merged without changes requested', icon: 'fa-check' },
'perfect-pr-5': { name: 'Clean Code', description: '5 PRs merged without changes requested', icon: 'fa-check-double' },
'perfect-pr-10': { name: 'Quality Author', description: '10 PRs merged without changes requested', icon: 'fa-circle-check' },
'perfect-pr-25': { name: 'Flawless', description: '25 PRs merged without changes requested', icon: 'fa-gem' },
// Streak achievements - Consistency is key
'streak-7': { name: 'Hot Streak', description: '7 day contribution streak', icon: 'fa-fire' },
'streak-30': { name: 'Ironclad', description: '30 day contribution streak', icon: 'fa-link' },
'streak-90': { name: 'Unbreakable', description: '90 day contribution streak', icon: 'fa-diamond' },
// ===== ACTIVE DAYS (Tiers: 7, 30, 60, 100) =====
'active-7': { name: 'Week Active', description: 'Active on 7 different days', icon: 'fa-calendar-day' },
'active-30': { name: 'Month Active', description: 'Active on 30 different days', icon: 'fa-calendar-week' },
'active-60': { name: 'Consistent Contributor', description: 'Active on 60 different days', icon: 'fa-chart-line' },
'active-100': { name: 'Dedicated Developer', description: 'Active on 100 different days', icon: 'fa-fire-flame-curved' },
// Time-based achievements - When you code matters
'early-bird': { name: 'Dawn Patrol', description: '50 commits before 9am', icon: 'fa-sun' },
'night-owl': { name: 'Nighthawk', description: '50 commits after 9pm', icon: 'fa-moon' },
'nosferatu': { name: 'Vampire', description: '25 commits between midnight and 4am', icon: 'fa-ghost' },
'weekend-warrior': { name: 'No Days Off', description: '25 weekend commits', icon: 'fa-calendar-xmark' },
// ===== LONGEST STREAK (Tiers: 3, 7, 14, 30) =====
'streak-3': { name: 'Getting Rolling', description: '3 day contribution streak', icon: 'fa-forward' },
'streak-7': { name: 'Week Warrior', description: '7 day contribution streak', icon: 'fa-calendar-week' },
'streak-14': { name: 'Two Week Streak', description: '14 day contribution streak', icon: 'fa-fire' },
'streak-30': { name: 'Month Master', description: '30 day contribution streak', icon: 'fa-calendar-check' },
// Activity achievements - Showing up matters
'active-30': { name: 'Reliable', description: 'Active on 30 different days', icon: 'fa-calendar-check' },
'active-100': { name: 'Stalwart', description: 'Active on 100 different days', icon: 'fa-tower-observation' },
'active-365': { name: 'Eternal', description: 'Active on 365 different days', icon: 'fa-sun-plant-wilt' }
// ===== WORK WEEK STREAK (Tiers: 3, 5, 10, 20) =====
'workweek-3': { name: 'Work Week Start', description: '3 consecutive weekday streak', icon: 'fa-briefcase' },
'workweek-5': { name: 'Full Work Week', description: '5 consecutive weekday streak', icon: 'fa-building' },
'workweek-10': { name: 'Two Week Grind', description: '10 consecutive weekday streak', icon: 'fa-business-time' },
'workweek-20': { name: 'Month of Mondays', description: '20 consecutive weekday streak', icon: 'fa-landmark' },
// ===== EARLY BIRD (Tiers: 10, 25, 50, 100) =====
'earlybird-10': { name: 'Early Riser', description: '10 commits before 9am', icon: 'fa-mug-hot' },
'earlybird-25': { name: 'Morning Person', description: '25 commits before 9am', icon: 'fa-cloud-sun' },
'earlybird-50': { name: 'Early Bird', description: '50 commits before 9am', icon: 'fa-sun' },
'earlybird-100': { name: 'Dawn Warrior', description: '100 commits before 9am', icon: 'fa-sunrise' },
// ===== NIGHT OWL (Tiers: 10, 25, 50, 100) =====
'nightowl-10': { name: 'Late Worker', description: '10 commits after 9pm', icon: 'fa-cloud-moon' },
'nightowl-25': { name: 'Evening Coder', description: '25 commits after 9pm', icon: 'fa-moon' },
'nightowl-50': { name: 'Night Owl', description: '50 commits after 9pm', icon: 'fa-star' },
'nightowl-100': { name: 'Nocturnal', description: '100 commits after 9pm', icon: 'fa-star-and-crescent' },
// ===== MIDNIGHT CODER (Tiers: 5, 10, 25, 50) =====
'midnight-5': { name: 'Night Shift', description: '5 commits between midnight and 4am', icon: 'fa-ghost' },
'midnight-10': { name: 'Insomniac', description: '10 commits between midnight and 4am', icon: 'fa-bed' },
'midnight-25': { name: 'Nosferatu', description: '25 commits between midnight and 4am', icon: 'fa-skull' },
'midnight-50': { name: 'Vampire Coder', description: '50 commits between midnight and 4am', icon: 'fa-skull-crossbones' },
// ===== WEEKEND WARRIOR (Tiers: 5, 10, 25, 50) =====
'weekend-5': { name: 'Weekend Work', description: '5 weekend commits', icon: 'fa-couch' },
'weekend-10': { name: 'Weekend Regular', description: '10 weekend commits', icon: 'fa-house-laptop' },
'weekend-25': { name: 'Weekend Warrior', description: '25 weekend commits', icon: 'fa-gamepad' },
'weekend-50': { name: 'No Days Off', description: '50 weekend commits', icon: 'fa-person-running' },
// ===== OUT OF HOURS (Tiers: 10, 25, 50, 100) =====
'ooh-10': { name: 'Extra Hours', description: '10 commits outside 9am-5pm', icon: 'fa-clock-rotate-left' },
'ooh-25': { name: 'Flexible Schedule', description: '25 commits outside 9am-5pm', icon: 'fa-user-clock' },
'ooh-50': { name: 'Off-Hours Hero', description: '50 commits outside 9am-5pm', icon: 'fa-hourglass-half' },
'ooh-100': { name: 'Time Bender', description: '100 commits outside 9am-5pm', icon: 'fa-infinity' },
// ===== DOCUMENTATION & COMMENTS ADDED (Tiers: 100, 500, 1000, 2500, 5000) =====
'docs-100': { name: 'Documenter', description: 'Added 100 lines of comments/docs', icon: 'fa-file-lines' },
'docs-500': { name: 'Technical Writer', description: 'Added 500 lines of comments/docs', icon: 'fa-book' },
'docs-1000': { name: 'Documentation Hero', description: 'Added 1000 lines of comments/docs', icon: 'fa-book-open' },
'docs-2500': { name: 'Knowledge Keeper', description: 'Added 2500 lines of comments/docs', icon: 'fa-scroll' },
'docs-5000': { name: 'Code Historian', description: 'Added 5000 lines of comments/docs', icon: 'fa-landmark' },
// ===== COMMENT CLEANUP (Tiers: 50, 200, 500, 1000, 2500) =====
'docs-del-50': { name: 'Comment Trimmer', description: 'Removed 50 lines of outdated comments', icon: 'fa-scissors' },
'docs-del-200': { name: 'Cleanup Crew', description: 'Removed 200 lines of outdated comments', icon: 'fa-broom' },
'docs-del-500': { name: 'Dead Code Hunter', description: 'Removed 500 lines of outdated comments', icon: 'fa-skull-crossbones' },
'docs-del-1000': { name: 'Comment Surgeon', description: 'Removed 1000 lines of outdated comments', icon: 'fa-user-doctor' },
'docs-del-2500': { name: 'Noise Eliminator', description: 'Removed 2500 lines of outdated comments', icon: 'fa-volume-xmark' },
}
const getAchievement = (id) => {
+137
View File
@@ -0,0 +1,137 @@
// Achievement category mappings and utilities
// Define achievement categories and their tier ordering (highest tier last)
const achievementCategories = {
// Commits
'commit': ['commit-1', 'commit-10', 'commit-50', 'commit-100', 'commit-500', 'commit-1000'],
// PRs opened
'pr': ['pr-1', 'pr-10', 'pr-25', 'pr-50', 'pr-100', 'pr-250'],
// Reviews
'review': ['review-1', 'review-10', 'review-25', 'review-50', 'review-100', 'review-250'],
// Review comments
'comment': ['comment-10', 'comment-50', 'comment-100', 'comment-250', 'comment-500'],
// Lines added
'lines-added': ['lines-added-100', 'lines-added-1000', 'lines-added-5000', 'lines-added-10000', 'lines-added-50000'],
// Lines deleted
'lines-deleted': ['lines-deleted-100', 'lines-deleted-500', 'lines-deleted-1000', 'lines-deleted-5000', 'lines-deleted-10000'],
// Review time
'review-time': ['review-time-24h', 'review-time-4h', 'review-time-1h'],
// Multi-repo
'repo': ['repo-2', 'repo-5', 'repo-10'],
// Unique reviewees
'reviewees': ['reviewees-3', 'reviewees-10', 'reviewees-25'],
// Large PRs
'large-pr': ['large-pr-500', 'large-pr-1000', 'large-pr-5000'],
// Small PRs
'small-pr': ['small-pr-5', 'small-pr-10', 'small-pr-25', 'small-pr-50'],
// Perfect PRs
'perfect-pr': ['perfect-pr-1', 'perfect-pr-5', 'perfect-pr-10', 'perfect-pr-25'],
// Active days
'active': ['active-7', 'active-30', 'active-60', 'active-100'],
// Streaks
'streak': ['streak-3', 'streak-7', 'streak-14', 'streak-30'],
// Work week streaks
'workweek': ['workweek-3', 'workweek-5', 'workweek-10', 'workweek-20'],
// Early bird
'earlybird': ['earlybird-10', 'earlybird-25', 'earlybird-50', 'earlybird-100'],
// Night owl
'nightowl': ['nightowl-10', 'nightowl-25', 'nightowl-50', 'nightowl-100'],
// Midnight coder
'midnight': ['midnight-5', 'midnight-10', 'midnight-25', 'midnight-50'],
// Weekend warrior
'weekend': ['weekend-5', 'weekend-10', 'weekend-25', 'weekend-50'],
// Out of hours
'ooh': ['ooh-10', 'ooh-25', 'ooh-50', 'ooh-100'],
// Documentation added
'docs': ['docs-100', 'docs-500', 'docs-1000', 'docs-2500', 'docs-5000'],
// Documentation deleted
'docs-del': ['docs-del-50', 'docs-del-200', 'docs-del-500', 'docs-del-1000', 'docs-del-2500'],
}
// Get the category for an achievement ID
export function getAchievementCategory(achievementId) {
for (const [category, tiers] of Object.entries(achievementCategories)) {
if (tiers.includes(achievementId)) {
return category
}
}
return null
}
// Get the tier index within a category (higher = better)
export function getAchievementTier(achievementId) {
const category = getAchievementCategory(achievementId)
if (!category) return -1
return achievementCategories[category].indexOf(achievementId)
}
/**
* Filter achievements to show only the highest tier in each category
* @param {string[]} achievements - Array of achievement IDs
* @returns {string[]} - Filtered array with only highest tier per category
*/
export function getHighestTierAchievements(achievements) {
if (!achievements || !achievements.length) return []
// Group achievements by category, keeping only the highest tier
const highestByCategory = {}
for (const achievementId of achievements) {
const category = getAchievementCategory(achievementId)
if (!category) {
// Unknown achievement, keep it
highestByCategory[achievementId] = { id: achievementId, tier: -1 }
continue
}
const tier = getAchievementTier(achievementId)
if (!highestByCategory[category] || tier > highestByCategory[category].tier) {
highestByCategory[category] = { id: achievementId, tier }
}
}
// Return just the achievement IDs, sorted by tier (highest first)
return Object.values(highestByCategory)
.sort((a, b) => b.tier - a.tier)
.map(item => item.id)
}
/**
* Get a priority score for sorting achievements (higher = more impressive)
* Categories are weighted to show most impressive achievements first
*/
const categoryPriority = {
'commit': 10,
'pr': 9,
'review': 8,
'lines-added': 7,
'perfect-pr': 6,
'streak': 5,
'active': 4,
'review-time': 3,
'docs': 2,
}
export function getAchievementPriority(achievementId) {
const category = getAchievementCategory(achievementId)
const basePriority = categoryPriority[category] || 0
const tier = getAchievementTier(achievementId)
// Combine category priority with tier (tier adds 0.1 per level)
return basePriority + (tier * 0.1)
}
/**
* Get highest tier achievements, sorted by importance
* @param {string[]} achievements - Array of achievement IDs
* @param {number} limit - Maximum number to return
* @returns {string[]} - Filtered and sorted array
*/
export function getTopAchievements(achievements, limit = 6) {
const highest = getHighestTierAchievements(achievements)
// Sort by priority (most impressive first)
highest.sort((a, b) => getAchievementPriority(b) - getAchievementPriority(a))
return highest.slice(0, limit)
}
+46 -2
View File
@@ -11,6 +11,7 @@ import AchievementProgress from '../components/AchievementProgress.vue'
import SectionHeader from '../components/SectionHeader.vue'
import GithubLink from '../components/GithubLink.vue'
import { formatNumber, formatPercent, formatDuration } from '../composables/formatters'
import { getHighestTierAchievements } from '../composables/achievements'
const route = useRoute()
const globalData = inject('globalData')
@@ -127,7 +128,7 @@ watch(globalData, loadContributor)
<div v-if="contributor.achievements?.length" class="mt-6 flex flex-wrap justify-center md:justify-start gap-3">
<AchievementBadge
v-for="achievement in contributor.achievements"
v-for="achievement in getHighestTierAchievements(contributor.achievements)"
:key="achievement"
:achievement-id="achievement"
size="lg"
@@ -194,6 +195,30 @@ watch(globalData, loadContributor)
-{{ formatNumber(contributor.lines_deleted || 0) }}
</span>
</div>
<div v-if="contributor.meaningful_lines_added !== undefined" class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Meaningful Lines Added</span>
<span class="text-emerald-500 font-semibold">
+{{ formatNumber(contributor.meaningful_lines_added || 0) }}
</span>
</div>
<div v-if="contributor.meaningful_lines_deleted !== undefined" class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Meaningful Lines Deleted</span>
<span class="text-rose-500 font-semibold">
-{{ formatNumber(contributor.meaningful_lines_deleted || 0) }}
</span>
</div>
<div v-if="contributor.comment_lines_added !== undefined" class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Comment Lines Added</span>
<span class="text-cyan-500 font-semibold">
+{{ formatNumber(contributor.comment_lines_added || 0) }}
</span>
</div>
<div v-if="contributor.comment_lines_deleted !== undefined" class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Comment Lines Deleted</span>
<span class="text-amber-500 font-semibold">
-{{ formatNumber(contributor.comment_lines_deleted || 0) }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Files Changed</span>
<span class="text-gray-800 dark:text-white font-semibold">
@@ -260,36 +285,55 @@ watch(globalData, loadContributor)
<i class="fas fa-chart-pie gradient-text mr-2"></i>Score Breakdown
</h3>
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4">
<div class="text-center p-4 rounded-lg bg-gray-50 dark:bg-gray-800/50">
<div class="text-2xl font-bold text-green-500">
{{ formatNumber(contributor.score.breakdown.commits || 0) }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">Commits</div>
<div class="text-xs text-gray-400 dark:text-gray-500">{{ contributor.commit_count || 0 }} × 10 pts</div>
</div>
<div class="text-center p-4 rounded-lg bg-gray-50 dark:bg-gray-800/50">
<div class="text-2xl font-bold text-blue-500">
{{ formatNumber(contributor.score.breakdown.prs || 0) }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">PRs</div>
<div class="text-xs text-gray-400 dark:text-gray-500">{{ contributor.prs_opened || 0 }} opened + {{ contributor.prs_merged || 0 }} merged</div>
</div>
<div class="text-center p-4 rounded-lg bg-gray-50 dark:bg-gray-800/50">
<div class="text-2xl font-bold text-purple-500">
{{ formatNumber(contributor.score.breakdown.reviews || 0) }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">Reviews</div>
<div class="text-xs text-gray-400 dark:text-gray-500">{{ contributor.reviews_given || 0 }} × 30 pts</div>
</div>
<div class="text-center p-4 rounded-lg bg-gray-50 dark:bg-gray-800/50">
<div class="text-2xl font-bold text-pink-500">
{{ formatNumber(contributor.score.breakdown.comments || 0) }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">Comments</div>
<div class="text-xs text-gray-400 dark:text-gray-500">{{ contributor.review_comments || 0 }} × 5 pts</div>
</div>
<div class="text-center p-4 rounded-lg bg-gray-50 dark:bg-gray-800/50">
<div class="text-2xl font-bold text-orange-500">
{{ formatNumber(contributor.score.breakdown.line_changes || 0) }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">Line Changes</div>
<div class="text-xs text-gray-400 dark:text-gray-500">meaningful lines × 0.1 pts</div>
</div>
<div class="text-center p-4 rounded-lg bg-gray-50 dark:bg-gray-800/50">
<div class="text-2xl font-bold text-yellow-500">
{{ formatNumber(contributor.score.breakdown.response_bonus || 0) }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">Response Bonus</div>
<div class="text-xs text-gray-400 dark:text-gray-500">fast review bonus</div>
</div>
<div class="text-center p-4 rounded-lg bg-gray-50 dark:bg-gray-800/50">
<div class="text-2xl font-bold text-indigo-500">
{{ formatNumber(contributor.score.breakdown.out_of_hours || 0) }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">Out of Hours</div>
<div class="text-xs text-gray-400 dark:text-gray-500">{{ contributor.out_of_hours_count || 0 }} × 2 pts</div>
</div>
</div>
</div>
+3 -2
View File
@@ -6,6 +6,7 @@ import ContributorRow from '../components/ContributorRow.vue'
import RankBadge from '../components/RankBadge.vue'
import AchievementBadge from '../components/AchievementBadge.vue'
import { formatNumber } from '../composables/formatters'
import { getHighestTierAchievements } from '../composables/achievements'
const globalData = inject('globalData')
const leaderboard = computed(() => globalData.value?.leaderboard || [])
@@ -59,9 +60,9 @@ const categoryIcon = (category) => {
</template>
<template #achievements="{ item }">
<div class="flex flex-wrap gap-1.5 max-w-[180px]">
<div class="flex flex-wrap gap-1.5 max-w-[280px]">
<AchievementBadge
v-for="achievement in (item.achievements || []).slice(0, 6)"
v-for="achievement in getHighestTierAchievements(item.achievements)"
:key="achievement"
:achievement-id="achievement"
size="sm"