- Created Document entity with properties for file management, including filename, originalFilename, mimeType, size, uploadedAt, uploadedBy, relatedEntity, relatedEntityId, and description. - Implemented DocumentRepository for querying documents by related entity and user. - Added GitRepository entity with properties for managing Git repositories, including URL, localPath, branch, provider, accessToken, project, lastSync, name, description, createdAt, and updatedAt. - Implemented GitRepositoryRepository for querying repositories by project. - Developed GitHubService for interacting with GitHub API, including methods for fetching commits, contributions, branches, and repository info. - Developed GitService for local Git repository interactions, including methods for fetching commits, contributions, branches, and repository info. - Developed GiteaService for interacting with Gitea API, including methods for fetching commits, contributions, branches, repository info, and testing connection.
292 lines
6.1 KiB
Vue
292 lines
6.1 KiB
Vue
<template>
|
|
<div class="git-contribution-chart">
|
|
<div class="flex justify-between align-items-center mb-4">
|
|
<h3 class="text-lg font-semibold m-0">Contributions im letzten Jahr</h3>
|
|
<div v-if="!loading && totalContributions > 0" class="text-500">
|
|
<i class="pi pi-calendar mr-2"></i>
|
|
{{ totalContributions }} Commits
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loading" class="text-center py-8">
|
|
<i class="pi pi-spin pi-spinner text-4xl text-primary"></i>
|
|
</div>
|
|
|
|
<div v-else-if="error" class="p-4 border-round surface-border border-1 text-center">
|
|
<i class="pi pi-exclamation-triangle text-orange-500 text-3xl mb-2"></i>
|
|
<p class="text-500">{{ error }}</p>
|
|
</div>
|
|
|
|
<div v-else-if="contributionData.length === 0" class="p-4 border-round surface-border border-1 text-center">
|
|
<i class="pi pi-info-circle text-blue-500 text-3xl mb-2"></i>
|
|
<p class="text-500">Keine Contributions im letzten Jahr</p>
|
|
</div>
|
|
|
|
<div v-else>
|
|
<!-- Heatmap-style visualization (similar to GitHub) -->
|
|
<div class="contribution-heatmap">
|
|
<div class="heatmap-grid">
|
|
<div
|
|
v-for="week in contributionData"
|
|
:key="week.week"
|
|
class="heatmap-cell"
|
|
:class="getIntensityClass(week.count)"
|
|
:title="`KW ${week.weekNumber} ${week.year}: ${week.count} Commits`"
|
|
>
|
|
<span class="cell-count">{{ week.count }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="legend mt-3 flex gap-2 align-items-center justify-end">
|
|
<span class="text-sm text-500">Weniger</span>
|
|
<div class="legend-cell level-0"></div>
|
|
<div class="legend-cell level-1"></div>
|
|
<div class="legend-cell level-2"></div>
|
|
<div class="legend-cell level-3"></div>
|
|
<div class="legend-cell level-4"></div>
|
|
<span class="text-sm text-500">Mehr</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bar Chart alternative -->
|
|
<div class="mt-4">
|
|
<Chart type="bar" :data="chartData" :options="chartOptions" class="h-20rem" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, watch } from 'vue'
|
|
import Chart from 'primevue/chart'
|
|
import { useToast } from 'primevue/usetoast'
|
|
|
|
const props = defineProps({
|
|
repositoryId: {
|
|
type: Number,
|
|
required: true
|
|
},
|
|
branch: {
|
|
type: String,
|
|
default: 'main'
|
|
},
|
|
author: {
|
|
type: String,
|
|
default: null
|
|
}
|
|
})
|
|
|
|
const toast = useToast()
|
|
const contributionData = ref([])
|
|
const loading = ref(false)
|
|
const error = ref(null)
|
|
|
|
const totalContributions = computed(() => {
|
|
return contributionData.value.reduce((sum, week) => sum + week.count, 0)
|
|
})
|
|
|
|
const chartData = computed(() => {
|
|
const labels = contributionData.value.map(w => `KW ${w.weekNumber}`)
|
|
const data = contributionData.value.map(w => w.count)
|
|
|
|
return {
|
|
labels,
|
|
datasets: [
|
|
{
|
|
label: 'Commits pro Woche',
|
|
data,
|
|
backgroundColor: '#10b981',
|
|
borderColor: '#059669',
|
|
borderWidth: 1
|
|
}
|
|
]
|
|
}
|
|
})
|
|
|
|
const chartOptions = ref({
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: (context) => {
|
|
return `${context.parsed.y} Commits`
|
|
}
|
|
}
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
grid: {
|
|
display: false
|
|
},
|
|
ticks: {
|
|
maxRotation: 45,
|
|
minRotation: 45
|
|
}
|
|
},
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
stepSize: 1
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
onMounted(() => {
|
|
loadContributions()
|
|
})
|
|
|
|
watch(() => [props.repositoryId, props.branch, props.author], () => {
|
|
loadContributions()
|
|
})
|
|
|
|
async function loadContributions() {
|
|
loading.value = true
|
|
error.value = null
|
|
|
|
try {
|
|
let url = `/api/git-repos/${props.repositoryId}/contributions?branch=${props.branch}`
|
|
if (props.author) {
|
|
url += `&author=${encodeURIComponent(props.author)}`
|
|
}
|
|
|
|
const response = await fetch(url)
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json()
|
|
throw new Error(errorData.error || 'Fehler beim Laden der Contributions')
|
|
}
|
|
|
|
const data = await response.json()
|
|
contributionData.value = data.contributions || []
|
|
} catch (err) {
|
|
console.error('Error loading contributions:', err)
|
|
error.value = err.message
|
|
toast.add({
|
|
severity: 'error',
|
|
summary: 'Fehler',
|
|
detail: err.message || 'Contributions konnten nicht geladen werden',
|
|
life: 3000
|
|
})
|
|
contributionData.value = []
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
function getIntensityClass(count) {
|
|
if (count === 0) return 'level-0'
|
|
if (count <= 2) return 'level-1'
|
|
if (count <= 5) return 'level-2'
|
|
if (count <= 10) return 'level-3'
|
|
return 'level-4'
|
|
}
|
|
|
|
defineExpose({
|
|
loadContributions
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.git-contribution-chart {
|
|
width: 100%;
|
|
}
|
|
|
|
.contribution-heatmap {
|
|
width: 100%;
|
|
}
|
|
|
|
.heatmap-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(16px, 1fr));
|
|
gap: 3px;
|
|
max-width: 100%;
|
|
}
|
|
|
|
.heatmap-cell {
|
|
position: relative;
|
|
aspect-ratio: 1;
|
|
border-radius: 3px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.heatmap-cell:hover {
|
|
transform: scale(1.2);
|
|
z-index: 10;
|
|
}
|
|
|
|
.cell-count {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
color: white;
|
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
opacity: 0;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.heatmap-cell:hover .cell-count {
|
|
opacity: 1;
|
|
}
|
|
|
|
.level-0 {
|
|
background-color: var(--surface-200);
|
|
}
|
|
|
|
.level-1 {
|
|
background-color: #9be9a8;
|
|
}
|
|
|
|
.level-2 {
|
|
background-color: #40c463;
|
|
}
|
|
|
|
.level-3 {
|
|
background-color: #30a14e;
|
|
}
|
|
|
|
.level-4 {
|
|
background-color: #216e39;
|
|
}
|
|
|
|
.legend {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.legend-cell {
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.legend-cell.level-0 {
|
|
background-color: var(--surface-200);
|
|
}
|
|
|
|
.legend-cell.level-1 {
|
|
background-color: #9be9a8;
|
|
}
|
|
|
|
.legend-cell.level-2 {
|
|
background-color: #40c463;
|
|
}
|
|
|
|
.legend-cell.level-3 {
|
|
background-color: #30a14e;
|
|
}
|
|
|
|
.legend-cell.level-4 {
|
|
background-color: #216e39;
|
|
}
|
|
</style>
|