Add score color logic and evaluation migration

- Introduced `getScoreColors` function to dynamically set badge colors based on skill scores for better visual feedback.
- Updated HomePage to display skill evaluation percentages with corresponding colors.
- Implemented `migrateEvaluation` function to ensure existing evaluations are updated with new skill categories, enhancing data integrity.
- Refactored data loading in `loadSkillCategories` and `loadTeams` to fetch from API endpoints, improving flexibility and maintainability.
This commit is contained in:
Julien Froidefond
2025-08-20 16:50:30 +02:00
parent 38d8e7ec40
commit ab9c35c276
5 changed files with 206 additions and 41 deletions

41
app/api/skills/route.ts Normal file
View File

@@ -0,0 +1,41 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
import { SkillCategory } from "@/lib/types";
export async function GET() {
try {
const dataDir = path.join(process.cwd(), "data", "skills");
const categories = [
"frontend",
"backend",
"devops",
"mobile",
"data",
"cloud",
"security",
"design",
];
const skillCategories: SkillCategory[] = [];
for (const category of categories) {
const filePath = path.join(dataDir, `${category}.json`);
if (fs.existsSync(filePath)) {
const fileContent = fs.readFileSync(filePath, "utf-8");
const data = JSON.parse(fileContent);
skillCategories.push(data);
}
}
return NextResponse.json(skillCategories);
} catch (error) {
console.error("Error loading skills:", error);
return NextResponse.json(
{ error: "Failed to load skills" },
{ status: 500 }
);
}
}

25
app/api/teams/route.ts Normal file
View File

@@ -0,0 +1,25 @@
import { NextResponse } from "next/server";
import fs from "fs";
import path from "path";
import { Team } from "@/lib/types";
export async function GET() {
try {
const filePath = path.join(process.cwd(), "data", "teams.json");
if (!fs.existsSync(filePath)) {
return NextResponse.json({ teams: [] });
}
const fileContent = fs.readFileSync(filePath, "utf-8");
const data = JSON.parse(fileContent);
return NextResponse.json(data.teams as Team[]);
} catch (error) {
console.error("Error loading teams:", error);
return NextResponse.json(
{ error: "Failed to load teams" },
{ status: 500 }
);
}
}

View File

@@ -20,6 +20,43 @@ import Link from "next/link";
import { Code2, ChevronDown, ChevronRight, ExternalLink } from "lucide-react";
import { getCategoryIcon } from "@/lib/category-icons";
// Fonction pour déterminer la couleur du badge selon le niveau moyen
function getScoreColors(score: number) {
if (score >= 2.5) {
// Expert/Maîtrise (violet)
return {
bg: "bg-violet-500/20",
border: "border-violet-500/30",
text: "text-violet-400",
gradient: "from-violet-500 to-violet-400",
};
} else if (score >= 1.5) {
// Autonome (vert)
return {
bg: "bg-green-500/20",
border: "border-green-500/30",
text: "text-green-400",
gradient: "from-green-500 to-green-400",
};
} else if (score >= 0.5) {
// Non autonome (orange/amber)
return {
bg: "bg-amber-500/20",
border: "border-amber-500/30",
text: "text-amber-400",
gradient: "from-amber-500 to-amber-400",
};
} else {
// Jamais pratiqué (rouge)
return {
bg: "bg-red-500/20",
border: "border-red-500/30",
text: "text-red-400",
gradient: "from-red-500 to-red-400",
};
}
}
export default function HomePage() {
const { userEvaluation, skillCategories, teams, loading, updateProfile } =
useEvaluation();
@@ -186,26 +223,53 @@ export default function HomePage() {
{category.category}
</h4>
</div>
<div className="px-2 py-0.5 rounded-full bg-blue-500/20 border border-blue-500/30">
<span className="text-xs font-medium text-blue-400">
{category.score.toFixed(1)}/3
</span>
</div>
{skillsCount > 0 ? (
(() => {
const colors = getScoreColors(category.score);
return (
<div
className={`px-2 py-0.5 rounded-full ${colors.bg} border ${colors.border}`}
>
<span
className={`text-xs font-medium ${colors.text}`}
>
{Math.round((category.score / 3) * 100)}%
</span>
</div>
);
})()
) : (
<div className="px-2 py-0.5 rounded-full bg-slate-500/20 border border-slate-500/30">
<span className="text-xs font-medium text-slate-400">
Aucune
</span>
</div>
)}
</div>
<div className="text-xs text-slate-400 mb-2">
{evaluatedCount}/{skillsCount} compétences sélectionnées
évaluées
</div>
<div className="w-full bg-white/10 rounded-full h-1.5">
<div
className="bg-gradient-to-r from-blue-500 to-blue-400 h-1.5 rounded-full transition-all"
style={{
width: `${
(category.score / category.maxScore) * 100
}%`,
}}
></div>
{skillsCount > 0
? `${evaluatedCount}/${skillsCount} compétences sélectionnées évaluées`
: "Aucune compétence sélectionnée"}
</div>
{skillsCount > 0 ? (
<div className="w-full bg-white/10 rounded-full h-1.5">
{(() => {
const colors = getScoreColors(category.score);
return (
<div
className={`bg-gradient-to-r ${colors.gradient} h-1.5 rounded-full transition-all`}
style={{
width: `${
(category.score / category.maxScore) * 100
}%`,
}}
></div>
);
})()}
</div>
) : (
<div className="w-full bg-slate-500/10 rounded-full h-1.5"></div>
)}
</div>
{/* Expanded Skills */}

View File

@@ -16,6 +16,40 @@ import {
} from "@/lib/evaluation-utils";
import { loadSkillCategories, loadTeams } from "@/lib/data-loader";
// Fonction pour migrer une évaluation existante avec de nouvelles catégories
function migrateEvaluation(
evaluation: UserEvaluation,
allCategories: SkillCategory[]
): UserEvaluation {
const existingCategoryNames = evaluation.evaluations.map((e) => e.category);
const missingCategories = allCategories.filter(
(cat) => !existingCategoryNames.includes(cat.category)
);
if (missingCategories.length === 0) {
return evaluation; // Pas de migration nécessaire
}
console.log(
"🔄 Migrating evaluation with new categories:",
missingCategories.map((c) => c.category)
);
const newCategoryEvaluations: CategoryEvaluation[] = missingCategories.map(
(category) => ({
category: category.category,
skills: [],
selectedSkillIds: [],
})
);
return {
...evaluation,
evaluations: [...evaluation.evaluations, ...newCategoryEvaluations],
lastUpdated: new Date().toISOString(),
};
}
export function useEvaluation() {
const [userEvaluation, setUserEvaluation] = useState<UserEvaluation | null>(
null
@@ -39,7 +73,10 @@ export function useEvaluation() {
// Try to load existing evaluation
const saved = loadUserEvaluation();
if (saved) {
setUserEvaluation(saved);
// Migrate evaluation to include new categories if needed
const migratedEvaluation = migrateEvaluation(saved, categories);
setUserEvaluation(migratedEvaluation);
saveUserEvaluation(migratedEvaluation); // Save the migrated version
}
} catch (error) {
console.error("Failed to initialize data:", error);

View File

@@ -1,29 +1,27 @@
import { SkillCategory, Team } from "./types";
// Import direct des données JSON depuis le dossier /data
import frontendData from "@/data/skills/frontend.json";
import backendData from "@/data/skills/backend.json";
import devopsData from "@/data/skills/devops.json";
import mobileData from "@/data/skills/mobile.json";
import dataData from "@/data/skills/data.json";
import cloudData from "@/data/skills/cloud.json";
import securityData from "@/data/skills/security.json";
import designData from "@/data/skills/design.json";
import teamsData from "@/data/teams.json";
export async function loadSkillCategories(): Promise<SkillCategory[]> {
return [
frontendData,
backendData,
devopsData,
mobileData,
dataData,
cloudData,
securityData,
designData,
] as SkillCategory[];
try {
const response = await fetch("/api/skills");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Failed to load skill categories:", error);
return [];
}
}
export async function loadTeams(): Promise<Team[]> {
return teamsData.teams as Team[];
try {
const response = await fetch("/api/teams");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Failed to load teams:", error);
return [];
}
}