Files
git-velocity/web/src/views/Contributor.vue
T

413 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { ref, computed, onMounted, watch, inject } from 'vue'
import { useRoute, RouterLink } from 'vue-router'
import PageHeader from '../components/PageHeader.vue'
import LoadingState from '../components/LoadingState.vue'
import ErrorState from '../components/ErrorState.vue'
import StatCard from '../components/StatCard.vue'
import Avatar from '../components/Avatar.vue'
import AchievementBadge from '../components/AchievementBadge.vue'
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')
const contributor = ref(null)
const loading = ref(true)
const error = ref(null)
const breadcrumbs = computed(() => [
{ label: 'Dashboard', to: '/' },
{ label: 'Contributors' },
{ label: contributor.value?.login || route.params.login }
])
async function loadContributor() {
loading.value = true
error.value = null
const login = route.params.login
try {
const response = await fetch(`./data/contributors/${login}.json`)
if (response.ok) {
const data = await response.json()
const leaderboard = globalData.value?.leaderboard || []
const leaderboardEntry = leaderboard.find(e => e.login === login)
if (leaderboardEntry) {
data.score = {
total: leaderboardEntry.score,
rank: leaderboardEntry.rank,
breakdown: data.score?.breakdown
}
data.achievements = leaderboardEntry.achievements
}
contributor.value = data
} else {
const leaderboard = globalData.value?.leaderboard || []
let found = leaderboard.find(e => e.login === login)
if (!found) {
const repos = globalData.value?.repositories || []
for (const repo of repos) {
const c = repo.contributors?.find(c => c.login === login)
if (c) {
found = c
break
}
}
}
if (found) {
contributor.value = found
} else {
error.value = 'Contributor not found'
}
}
} catch (e) {
error.value = `Failed to load contributor: ${e.message}`
}
loading.value = false
}
onMounted(loadContributor)
watch(() => route.params, loadContributor)
watch(globalData, loadContributor)
</script>
<template>
<div>
<LoadingState v-if="loading" message="Loading contributor..." />
<ErrorState v-else-if="error" :message="error" />
<template v-else-if="contributor">
<!-- Profile Header -->
<header class="py-12 px-4">
<div class="container mx-auto">
<PageHeader :breadcrumbs="breadcrumbs" :title="''" />
<div class="flex flex-col md:flex-row items-center md:items-start space-y-4 md:space-y-0 md:space-x-8">
<Avatar
:src="contributor.avatar_url"
:name="contributor.login"
size="2xl"
class="shadow-modern"
/>
<div class="text-center md:text-left">
<h1 class="text-4xl font-bold gradient-text">
{{ contributor.name || contributor.login }}
</h1>
<p class="text-xl text-gray-500 dark:text-gray-400 mt-1">
<GithubLink :url="`https://github.com/${contributor.login}`">
@{{ contributor.login }}
</GithubLink>
</p>
<div class="flex items-center justify-center md:justify-start space-x-4 mt-4">
<div class="score-card rounded-lg px-4 py-2">
<span class="text-sm text-gray-500 dark:text-gray-400">Score:</span>
<span class="text-2xl font-bold gradient-text ml-2">
{{ formatNumber(contributor.score?.total || contributor.score || 0) }}
</span>
</div>
<div v-if="contributor.score?.rank" class="text-sm text-gray-500 dark:text-gray-400">
Rank #{{ contributor.score.rank }}
<span v-if="contributor.score?.percentile_rank">
(Top {{ formatPercent(contributor.score.percentile_rank) }})
</span>
</div>
</div>
<div v-if="contributor.achievements?.length" class="mt-6 flex flex-wrap justify-center md:justify-start gap-3">
<AchievementBadge
v-for="achievement in getHighestTierAchievements(contributor.achievements)"
:key="achievement"
:achievement-id="achievement"
size="lg"
show-label
/>
</div>
</div>
</div>
</div>
</header>
<!-- Stats Grid -->
<section class="py-8 px-4">
<div class="container mx-auto">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<StatCard
:value="contributor.commit_count || 0"
label="Commits"
icon="fas fa-code-commit"
icon-color="text-green-500"
/>
<StatCard
:value="contributor.prs_opened || 0"
label="PRs Opened"
icon="fas fa-code-pull-request"
icon-color="text-blue-500"
/>
<StatCard
:value="contributor.prs_merged || 0"
label="PRs Merged"
icon="fas fa-code-merge"
icon-color="text-purple-500"
/>
<StatCard
:value="contributor.reviews_given || 0"
label="Reviews Given"
icon="fas fa-eye"
icon-color="text-orange-500"
/>
</div>
</div>
</section>
<!-- Detailed Stats -->
<section class="py-8 px-4">
<div class="container mx-auto">
<div class="grid md:grid-cols-2 gap-6">
<!-- Code Stats -->
<div class="card">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">
<i class="fas fa-code text-green-500 mr-2"></i>Code Contributions
</h3>
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Lines Added</span>
<span class="text-green-500 font-semibold">
+{{ formatNumber(contributor.lines_added || 0) }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Lines Deleted</span>
<span class="text-red-500 font-semibold">
-{{ 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">
{{ formatNumber(contributor.files_changed || 0) }}
</span>
</div>
<div v-if="contributor.avg_pr_size" class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Avg PR Size</span>
<span class="text-gray-800 dark:text-white font-semibold">
{{ formatNumber(Math.round(contributor.avg_pr_size)) }} lines
</span>
</div>
</div>
</div>
<!-- Review Stats -->
<div class="card">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">
<i class="fas fa-comments text-purple-500 mr-2"></i>Review Activity
</h3>
<div class="space-y-4">
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Reviews Given</span>
<span class="text-gray-800 dark:text-white font-semibold">
{{ formatNumber(contributor.reviews_given || 0) }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Approvals</span>
<span class="text-green-500 font-semibold">
{{ formatNumber(contributor.approvals_given || 0) }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Changes Requested</span>
<span class="text-orange-500 font-semibold">
{{ formatNumber(contributor.changes_requested || 0) }}
</span>
</div>
<div class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Review Comments</span>
<span class="text-gray-800 dark:text-white font-semibold">
{{ formatNumber(contributor.review_comments || 0) }}
</span>
</div>
<div v-if="contributor.avg_review_time_hours" class="flex items-center justify-between">
<span class="text-gray-600 dark:text-gray-300">Avg Review Time</span>
<span class="text-gray-800 dark:text-white font-semibold">
{{ formatDuration(contributor.avg_review_time_hours) }}
</span>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Score Breakdown -->
<section v-if="contributor.score?.breakdown" class="py-8 px-4">
<div class="container mx-auto">
<div class="card">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-4">
<i class="fas fa-chart-pie gradient-text mr-2"></i>Score Breakdown
</h3>
<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>
</div>
</section>
<!-- Achievement Progress Section -->
<section class="py-8 px-4">
<div class="container mx-auto">
<div class="grid md:grid-cols-2 gap-6">
<!-- Earned Achievements -->
<div v-if="contributor.achievements?.length" class="card">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white">
<i class="fas fa-award gradient-text mr-2"></i>Achievements Earned
</h3>
<span class="px-2.5 py-1 rounded-full bg-gradient-to-r from-yellow-400 to-amber-500 text-white text-sm font-bold shadow-md">
{{ contributor.achievements.length }}
</span>
</div>
<div class="grid grid-cols-4 sm:grid-cols-5 gap-3">
<div
v-for="achievement in contributor.achievements"
:key="achievement"
class="flex flex-col items-center p-2 rounded-xl bg-gray-50 dark:bg-gray-800/50 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
<AchievementBadge
:achievement-id="achievement"
size="md"
show-label
/>
</div>
</div>
</div>
<!-- Progress to Next Achievements -->
<div class="card">
<h3 class="text-lg font-semibold text-gray-800 dark:text-white mb-6">
<i class="fas fa-chart-line text-primary-500 mr-2"></i>Next Achievements
</h3>
<AchievementProgress
:contributor="contributor"
:max-display="6"
/>
</div>
</div>
</div>
</section>
<!-- Repositories Contributed -->
<section v-if="contributor.repositories_contributed?.length" class="py-8 px-4">
<div class="container mx-auto">
<SectionHeader
:title="`Contributed to ${contributor.repositories_contributed.length} Repositories`"
icon="fas fa-folder-tree"
icon-color="text-blue-500"
/>
<div class="flex flex-wrap gap-2">
<RouterLink
v-for="repo in contributor.repositories_contributed"
:key="repo"
:to="`/repos/${repo}`"
class="inline-flex items-center px-3 py-1.5 rounded-full text-sm bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 hover:bg-primary-100 dark:hover:bg-primary-900/30 hover:text-primary-700 dark:hover:text-primary-300 transition-colors"
>
<i class="fas fa-code-branch text-gray-400 mr-2"></i>
{{ repo }}
</RouterLink>
</div>
</div>
</section>
</template>
</div>
</template>