Add skills page with category grouping (Story 2.4)

- Enhance Skill model with getCurrentLevel() and ordered scope
- Update SkillController to group by category with translated labels
- Add level and project_count to SkillResource
- Create skill.ts types (Skill, SkillCategory, SkillsResponse)
- Create useFetchSkills composable
- Create SkillCard component with animated progress bar
- Implement competences.vue with:
  - Responsive grid (2/3/4 columns)
  - Category sections with icons
  - Stagger animations (respects prefers-reduced-motion)
  - Loading/error/empty states
  - Placeholder for vis.js skill tree (Epic 3)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-06 10:37:10 +01:00
parent 2269ecdb62
commit 4db96a0ded
11 changed files with 414 additions and 65 deletions

View File

@@ -0,0 +1,83 @@
<template>
<button
type="button"
class="skill-card group w-full text-left bg-sky-text/5 rounded-xl p-4 hover:bg-sky-text/10 transition-colors cursor-pointer border border-sky-text/10"
@click="emit('click', skill)"
>
<div class="flex items-center gap-3 mb-3">
<!-- Icon -->
<div class="w-10 h-10 flex items-center justify-center bg-sky-dark/50 rounded-lg shrink-0">
<span v-if="skill.icon" class="text-2xl">{{ skill.icon }}</span>
<span v-else class="text-sky-text/40 text-lg">💻</span>
</div>
<!-- Name and project count -->
<div class="flex-1 min-w-0">
<h3 class="font-ui font-medium text-sky-text truncate group-hover:text-sky-accent transition-colors">
{{ skill.name }}
</h3>
<p v-if="skill.project_count" class="text-xs text-sky-text/50">
{{ skill.project_count }} {{ skill.project_count > 1 ? $t('skills.projects') : $t('skills.project') }}
</p>
</div>
</div>
<!-- Progress bar -->
<div class="relative h-2 bg-sky-dark/50 rounded-full overflow-hidden">
<div
class="skill-progress absolute left-0 top-0 h-full bg-gradient-to-r from-sky-accent to-sky-accent/80 rounded-full"
:style="{ '--target-width': `${progressPercent}%` }"
/>
</div>
<!-- Level display -->
<div class="flex justify-between items-center mt-2">
<span class="text-xs text-sky-text/40 font-ui">{{ $t('skills.level') }}</span>
<span class="text-sm font-ui font-medium text-sky-accent">
{{ skill.level }}/{{ skill.max_level }}
</span>
</div>
</button>
</template>
<script setup lang="ts">
import type { Skill } from '~/types/skill'
const props = defineProps<{
skill: Skill
}>()
const emit = defineEmits<{
click: [skill: Skill]
}>()
const progressPercent = computed(() =>
Math.round((props.skill.level / props.skill.max_level) * 100),
)
</script>
<style scoped>
.skill-card:focus-visible {
outline: 2px solid var(--color-sky-accent, #38bdf8);
outline-offset: 2px;
}
.skill-progress {
width: 0;
animation: fillProgress 0.8s ease-out forwards;
animation-delay: var(--animation-delay, 0ms);
}
@keyframes fillProgress {
to {
width: var(--target-width, 0%);
}
}
@media (prefers-reduced-motion: reduce) {
.skill-progress {
animation: none;
width: var(--target-width, 0%);
}
}
</style>