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:
41
app/api/skills/route.ts
Normal file
41
app/api/skills/route.ts
Normal 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
25
app/api/teams/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
app/page.tsx
76
app/page.tsx
@@ -20,6 +20,43 @@ import Link from "next/link";
|
|||||||
import { Code2, ChevronDown, ChevronRight, ExternalLink } from "lucide-react";
|
import { Code2, ChevronDown, ChevronRight, ExternalLink } from "lucide-react";
|
||||||
import { getCategoryIcon } from "@/lib/category-icons";
|
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() {
|
export default function HomePage() {
|
||||||
const { userEvaluation, skillCategories, teams, loading, updateProfile } =
|
const { userEvaluation, skillCategories, teams, loading, updateProfile } =
|
||||||
useEvaluation();
|
useEvaluation();
|
||||||
@@ -186,26 +223,53 @@ export default function HomePage() {
|
|||||||
{category.category}
|
{category.category}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 py-0.5 rounded-full bg-blue-500/20 border border-blue-500/30">
|
{skillsCount > 0 ? (
|
||||||
<span className="text-xs font-medium text-blue-400">
|
(() => {
|
||||||
{category.score.toFixed(1)}/3
|
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>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
<div className="text-xs text-slate-400 mb-2">
|
<div className="text-xs text-slate-400 mb-2">
|
||||||
{evaluatedCount}/{skillsCount} compétences sélectionnées
|
{skillsCount > 0
|
||||||
évaluées
|
? `${evaluatedCount}/${skillsCount} compétences sélectionnées évaluées`
|
||||||
|
: "Aucune compétence sélectionnée"}
|
||||||
</div>
|
</div>
|
||||||
|
{skillsCount > 0 ? (
|
||||||
<div className="w-full bg-white/10 rounded-full h-1.5">
|
<div className="w-full bg-white/10 rounded-full h-1.5">
|
||||||
|
{(() => {
|
||||||
|
const colors = getScoreColors(category.score);
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className="bg-gradient-to-r from-blue-500 to-blue-400 h-1.5 rounded-full transition-all"
|
className={`bg-gradient-to-r ${colors.gradient} h-1.5 rounded-full transition-all`}
|
||||||
style={{
|
style={{
|
||||||
width: `${
|
width: `${
|
||||||
(category.score / category.maxScore) * 100
|
(category.score / category.maxScore) * 100
|
||||||
}%`,
|
}%`,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-full bg-slate-500/10 rounded-full h-1.5"></div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expanded Skills */}
|
{/* Expanded Skills */}
|
||||||
|
|||||||
@@ -16,6 +16,40 @@ import {
|
|||||||
} from "@/lib/evaluation-utils";
|
} from "@/lib/evaluation-utils";
|
||||||
import { loadSkillCategories, loadTeams } from "@/lib/data-loader";
|
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() {
|
export function useEvaluation() {
|
||||||
const [userEvaluation, setUserEvaluation] = useState<UserEvaluation | null>(
|
const [userEvaluation, setUserEvaluation] = useState<UserEvaluation | null>(
|
||||||
null
|
null
|
||||||
@@ -39,7 +73,10 @@ export function useEvaluation() {
|
|||||||
// Try to load existing evaluation
|
// Try to load existing evaluation
|
||||||
const saved = loadUserEvaluation();
|
const saved = loadUserEvaluation();
|
||||||
if (saved) {
|
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) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize data:", error);
|
console.error("Failed to initialize data:", error);
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
import { SkillCategory, Team } from "./types";
|
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[]> {
|
export async function loadSkillCategories(): Promise<SkillCategory[]> {
|
||||||
return [
|
try {
|
||||||
frontendData,
|
const response = await fetch("/api/skills");
|
||||||
backendData,
|
if (!response.ok) {
|
||||||
devopsData,
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
mobileData,
|
}
|
||||||
dataData,
|
return await response.json();
|
||||||
cloudData,
|
} catch (error) {
|
||||||
securityData,
|
console.error("Failed to load skill categories:", error);
|
||||||
designData,
|
return [];
|
||||||
] as SkillCategory[];
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadTeams(): Promise<Team[]> {
|
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 [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user