feat: handling SSR on evaluation

This commit is contained in:
Julien Froidefond
2025-08-21 13:19:46 +02:00
parent 69f23db55d
commit ef16c73625
7 changed files with 481 additions and 135 deletions

View File

@@ -1,28 +1,40 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { evaluationService } from "@/services/evaluation-service"; import { evaluationService } from "@/services/evaluation-service";
import { UserProfile, SkillLevel } from "@/lib/types"; import { UserProfile, SkillLevel } from "@/lib/types";
const COOKIE_NAME = "peakSkills_userId";
export async function PUT(request: NextRequest) { export async function PUT(request: NextRequest) {
try { try {
const body = await request.json(); // Récupérer l'utilisateur depuis le cookie
const { const cookieStore = await cookies();
profile, const userId = cookieStore.get(COOKIE_NAME)?.value;
category,
skillId,
level,
canMentor,
wantsToLearn,
action,
} = body;
if (!profile || !category || !skillId) { if (!userId) {
return NextResponse.json( return NextResponse.json(
{ error: "profile, category et skillId sont requis" }, { error: "Utilisateur non authentifié" },
{ status: 400 } { status: 401 }
); );
} }
const userProfile: UserProfile = profile; const userProfile = await evaluationService.getUserById(parseInt(userId));
if (!userProfile) {
return NextResponse.json(
{ error: "Utilisateur introuvable" },
{ status: 404 }
);
}
const body = await request.json();
const { category, skillId, level, canMentor, wantsToLearn, action } = body;
if (!category || !skillId) {
return NextResponse.json(
{ error: "category et skillId sont requis" },
{ status: 400 }
);
}
switch (action) { switch (action) {
case "updateLevel": case "updateLevel":

View File

@@ -1,106 +1,49 @@
"use client"; import { redirect } from "next/navigation";
import { useEffect } from "react";
import { useEvaluation } from "@/hooks/use-evaluation";
import { ProfileForm } from "@/components/profile-form";
import { SkillEvaluation } from "@/components/skill-evaluation";
import { useUser } from "@/hooks/use-user-context";
import { import {
Card, isUserAuthenticated,
CardContent, getServerUserEvaluation,
CardDescription, getServerSkillCategories,
CardHeader, getServerTeams,
CardTitle, } from "@/lib/server-auth";
} from "@/components/ui/card"; import {
EvaluationClientWrapper,
WelcomeEvaluationScreen,
} from "@/components/evaluation";
import { SkillEvaluation } from "@/components/skill-evaluation";
export default function EvaluationPage() { export default async function EvaluationPage() {
const { // Vérifier l'authentification
userEvaluation, const isAuthenticated = await isUserAuthenticated();
skillCategories,
teams,
loading,
updateProfile,
updateSkillLevel,
updateSkillMentorStatus,
updateSkillLearningStatus,
addSkillToEvaluation,
removeSkillFromEvaluation,
initializeEmptyEvaluation,
} = useEvaluation();
const { setUserInfo } = useUser(); // Si pas de cookie d'authentification, rediriger vers login
if (!isAuthenticated) {
// Update user info in navigation when user evaluation is loaded redirect("/login");
useEffect(() => {
if (userEvaluation) {
const teamName =
teams.find((t) => t.id === userEvaluation.profile.teamId)?.name || "";
setUserInfo({
firstName: userEvaluation.profile.firstName,
lastName: userEvaluation.profile.lastName,
teamName,
});
} else {
setUserInfo(null);
}
}, [userEvaluation, teams, setUserInfo]);
if (loading) {
return (
<div className="container mx-auto py-8">
<div className="flex items-center justify-center min-h-[400px]">
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div>
<p>Chargement...</p>
</div>
</div>
</div>
);
} }
// If no user evaluation exists, show profile form // Charger les données côté serveur
const [userEvaluation, skillCategories, teams] = await Promise.all([
getServerUserEvaluation(),
getServerSkillCategories(),
getServerTeams(),
]);
// Si pas d'évaluation, afficher l'écran d'accueil évaluation
if (!userEvaluation) { if (!userEvaluation) {
const handleProfileSubmit = (profile: any) => { return <WelcomeEvaluationScreen teams={teams} />;
initializeEmptyEvaluation(profile);
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 relative overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-900/20 via-slate-900 to-slate-950" />
<div className="absolute inset-0 bg-grid-white/5 bg-[size:50px_50px]" />
<div className="relative z-10 container mx-auto py-16 px-6">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold mb-4 text-white">
Commencer l'évaluation
</h1>
<p className="text-lg text-slate-400 mb-8">
Renseignez vos informations pour débuter votre auto-évaluation
</p>
</div>
<div className="max-w-2xl mx-auto">
<ProfileForm teams={teams} onSubmit={handleProfileSubmit} />
</div>
</div>
</div>
);
} }
return ( return (
<EvaluationClientWrapper userEvaluation={userEvaluation} teams={teams}>
<div> <div>
{/* Skill Evaluation */} {/* Skill Evaluation */}
{skillCategories.length > 0 && userEvaluation.evaluations.length > 0 && ( {skillCategories.length > 0 &&
userEvaluation.evaluations.length > 0 && (
<SkillEvaluation <SkillEvaluation
categories={skillCategories} categories={skillCategories}
evaluations={userEvaluation.evaluations} evaluations={userEvaluation.evaluations}
onUpdateSkill={updateSkillLevel}
onUpdateMentorStatus={updateSkillMentorStatus}
onUpdateLearningStatus={updateSkillLearningStatus}
onAddSkill={addSkillToEvaluation}
onRemoveSkill={removeSkillFromEvaluation}
/> />
)} )}
</div> </div>
</EvaluationClientWrapper>
); );
} }

View File

@@ -0,0 +1,155 @@
"use client";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { useUser } from "@/hooks/use-user-context";
import { UserEvaluation, Team } from "@/lib/types";
import {
updateSkillLevel as updateSkillLevelAction,
updateSkillMentorStatus as updateSkillMentorStatusAction,
updateSkillLearningStatus as updateSkillLearningStatusAction,
addSkillToEvaluation as addSkillToEvaluationAction,
removeSkillFromEvaluation as removeSkillFromEvaluationAction,
} from "@/lib/evaluation-actions";
interface EvaluationClientWrapperProps {
userEvaluation: UserEvaluation;
teams: Team[];
children: React.ReactNode;
}
export function EvaluationClientWrapper({
userEvaluation,
teams,
children,
}: EvaluationClientWrapperProps) {
const { setUserInfo } = useUser();
const router = useRouter();
// Wrapper functions that refresh the page after API calls
const updateSkillLevel = async (
category: string,
skillId: string,
level: any
) => {
await updateSkillLevelAction(category, skillId, level);
router.refresh();
};
const updateSkillMentorStatus = async (
category: string,
skillId: string,
canMentor: boolean
) => {
await updateSkillMentorStatusAction(category, skillId, canMentor);
router.refresh();
};
const updateSkillLearningStatus = async (
category: string,
skillId: string,
wantsToLearn: boolean
) => {
await updateSkillLearningStatusAction(category, skillId, wantsToLearn);
router.refresh();
};
const addSkillToEvaluation = async (category: string, skillId: string) => {
await addSkillToEvaluationAction(category, skillId);
router.refresh();
};
const removeSkillFromEvaluation = async (
category: string,
skillId: string
) => {
await removeSkillFromEvaluationAction(category, skillId);
router.refresh();
};
// Update user info in navigation when user evaluation is loaded
useEffect(() => {
if (userEvaluation) {
const teamName =
teams.find((t) => t.id === userEvaluation.profile.teamId)?.name || "";
setUserInfo({
firstName: userEvaluation.profile.firstName,
lastName: userEvaluation.profile.lastName,
teamName,
});
} else {
setUserInfo(null);
}
}, [userEvaluation, teams, setUserInfo]);
// Provide evaluation functions to children through React context or props
return (
<EvaluationProvider
updateSkillLevel={updateSkillLevel}
updateSkillMentorStatus={updateSkillMentorStatus}
updateSkillLearningStatus={updateSkillLearningStatus}
addSkillToEvaluation={addSkillToEvaluation}
removeSkillFromEvaluation={removeSkillFromEvaluation}
>
{children}
</EvaluationProvider>
);
}
// Simple context provider for evaluation functions
import { createContext, useContext } from "react";
interface EvaluationContextType {
updateSkillLevel: (categoryId: string, skillId: string, level: any) => void;
updateSkillMentorStatus: (
categoryId: string,
skillId: string,
canMentor: boolean
) => void;
updateSkillLearningStatus: (
categoryId: string,
skillId: string,
wantsToLearn: boolean
) => void;
addSkillToEvaluation: (categoryId: string, skillId: string) => void;
removeSkillFromEvaluation: (categoryId: string, skillId: string) => void;
}
const EvaluationContext = createContext<EvaluationContextType | undefined>(
undefined
);
function EvaluationProvider({
children,
updateSkillLevel,
updateSkillMentorStatus,
updateSkillLearningStatus,
addSkillToEvaluation,
removeSkillFromEvaluation,
}: {
children: React.ReactNode;
} & EvaluationContextType) {
return (
<EvaluationContext.Provider
value={{
updateSkillLevel,
updateSkillMentorStatus,
updateSkillLearningStatus,
addSkillToEvaluation,
removeSkillFromEvaluation,
}}
>
{children}
</EvaluationContext.Provider>
);
}
export function useEvaluationContext() {
const context = useContext(EvaluationContext);
if (context === undefined) {
throw new Error(
"useEvaluationContext must be used within an EvaluationProvider"
);
}
return context;
}

View File

@@ -1,5 +1,9 @@
export {
EvaluationClientWrapper,
useEvaluationContext,
} from "./client-wrapper";
export { WelcomeEvaluationScreen } from "./welcome-screen";
export { EvaluationHeader } from "./evaluation-header"; export { EvaluationHeader } from "./evaluation-header";
export { SkillLevelLegend } from "./skill-level-legend";
export { CategoryTabs } from "./category-tabs"; export { CategoryTabs } from "./category-tabs";
export { SkillEvaluationGrid } from "./skill-evaluation-grid"; export { SkillEvaluationGrid } from "./skill-evaluation-grid";
export { SkillEvaluationCard } from "./skill-evaluation-card"; export { SkillLevelLegend } from "./skill-level-legend";

View File

@@ -0,0 +1,73 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import { ProfileForm } from "@/components/profile-form";
import { initializeEmptyEvaluation } from "@/lib/evaluation-actions";
import { Team, UserProfile } from "@/lib/types";
import { Code2 } from "lucide-react";
interface WelcomeEvaluationScreenProps {
teams: Team[];
}
export function WelcomeEvaluationScreen({
teams,
}: WelcomeEvaluationScreenProps) {
const [isSubmitting, setIsSubmitting] = useState(false);
const router = useRouter();
const handleProfileSubmit = async (profile: UserProfile) => {
setIsSubmitting(true);
try {
await initializeEmptyEvaluation(profile);
// Rafraîchir la page pour que le SSR prenne en compte la nouvelle évaluation
router.refresh();
} catch (error) {
console.error("Failed to initialize evaluation:", error);
} finally {
setIsSubmitting(false);
}
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 relative overflow-hidden">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-blue-900/20 via-slate-900 to-slate-950" />
<div className="absolute inset-0 bg-grid-white/5 bg-[size:50px_50px]" />
<div className="relative z-10 container mx-auto py-16 px-6">
<div className="text-center mb-12">
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 backdrop-blur-sm mb-6">
<Code2 className="h-4 w-4 text-blue-400" />
<span className="text-sm font-medium text-slate-200">
PeakSkills - Évaluation
</span>
</div>
<h1 className="text-4xl font-bold mb-4 text-white">
Commencer l'évaluation
</h1>
<p className="text-lg text-slate-400 mb-8">
Renseignez vos informations pour débuter votre auto-évaluation
</p>
</div>
<div className="max-w-2xl mx-auto">
<div className="relative">
{isSubmitting && (
<div className="absolute inset-0 bg-black/20 backdrop-blur-sm z-10 flex items-center justify-center rounded-lg">
<div className="text-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-400 mx-auto mb-2"></div>
<p className="text-white text-sm">
Initialisation de l'évaluation...
</p>
</div>
</div>
)}
<ProfileForm teams={teams} onSubmit={handleProfileSubmit} />
</div>
</div>
</div>
</div>
);
}

View File

@@ -5,6 +5,7 @@ import { useSearchParams, useRouter } from "next/navigation";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { SkillCategory, SkillLevel, CategoryEvaluation } from "@/lib/types"; import { SkillCategory, SkillLevel, CategoryEvaluation } from "@/lib/types";
import { SkillSelector } from "./skill-selector"; import { SkillSelector } from "./skill-selector";
import { useEvaluationContext } from "./evaluation";
import { import {
EvaluationHeader, EvaluationHeader,
SkillLevelLegend, SkillLevelLegend,
@@ -15,30 +16,19 @@ import {
interface SkillEvaluationProps { interface SkillEvaluationProps {
categories: SkillCategory[]; categories: SkillCategory[];
evaluations: CategoryEvaluation[]; evaluations: CategoryEvaluation[];
onUpdateSkill: (category: string, skillId: string, level: SkillLevel) => void;
onUpdateMentorStatus: (
category: string,
skillId: string,
canMentor: boolean
) => void;
onUpdateLearningStatus: (
category: string,
skillId: string,
wantsToLearn: boolean
) => void;
onAddSkill: (category: string, skillId: string) => void;
onRemoveSkill: (category: string, skillId: string) => void;
} }
export function SkillEvaluation({ export function SkillEvaluation({
categories, categories,
evaluations, evaluations,
onUpdateSkill,
onUpdateMentorStatus,
onUpdateLearningStatus,
onAddSkill,
onRemoveSkill,
}: SkillEvaluationProps) { }: SkillEvaluationProps) {
const {
updateSkillLevel,
updateSkillMentorStatus,
updateSkillLearningStatus,
addSkillToEvaluation,
removeSkillFromEvaluation,
} = useEvaluationContext();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
const categoryParam = searchParams.get("category"); const categoryParam = searchParams.get("category");
@@ -97,18 +87,18 @@ export function SkillEvaluation({
categories={categories} categories={categories}
evaluations={evaluations} evaluations={evaluations}
selectedCategory={selectedCategory} selectedCategory={selectedCategory}
onAddSkill={onAddSkill} onAddSkill={addSkillToEvaluation}
onRemoveSkill={onRemoveSkill} onRemoveSkill={removeSkillFromEvaluation}
/> />
{currentEvaluation && ( {currentEvaluation && (
<SkillEvaluationGrid <SkillEvaluationGrid
currentCategory={currentCategory} currentCategory={currentCategory}
currentEvaluation={currentEvaluation} currentEvaluation={currentEvaluation}
onUpdateSkill={onUpdateSkill} onUpdateSkill={updateSkillLevel}
onUpdateMentorStatus={onUpdateMentorStatus} onUpdateMentorStatus={updateSkillMentorStatus}
onUpdateLearningStatus={onUpdateLearningStatus} onUpdateLearningStatus={updateSkillLearningStatus}
onRemoveSkill={onRemoveSkill} onRemoveSkill={removeSkillFromEvaluation}
/> />
)} )}
</div> </div>

169
lib/evaluation-actions.ts Normal file
View File

@@ -0,0 +1,169 @@
"use client";
import { SkillLevel } from "@/lib/types";
/**
* Actions d'évaluation standalone pour SSR
* Ces fonctions utilisent directement l'API sans refetch de toutes les données
*/
export async function updateSkillLevel(
category: string,
skillId: string,
level: SkillLevel
): Promise<void> {
try {
const response = await fetch("/api/evaluations/skills", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
credentials: "same-origin",
body: JSON.stringify({
action: "updateLevel",
category,
skillId,
level,
}),
});
if (!response.ok) {
const errorData = await response.text();
console.error("API Error:", response.status, errorData);
throw new Error(
`Failed to update skill level: ${response.status} - ${errorData}`
);
}
// Note: La page se rafraîchira via router.refresh() appelé par le composant
} catch (error) {
console.error("Failed to update skill level:", error);
throw error;
}
}
export async function updateSkillMentorStatus(
category: string,
skillId: string,
canMentor: boolean
): Promise<void> {
try {
const response = await fetch("/api/evaluations/skills", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
credentials: "same-origin",
body: JSON.stringify({
action: "updateMentorStatus",
category,
skillId,
canMentor,
}),
});
if (!response.ok) {
throw new Error("Failed to update skill mentor status");
}
} catch (error) {
console.error("Failed to update skill mentor status:", error);
throw error;
}
}
export async function updateSkillLearningStatus(
category: string,
skillId: string,
wantsToLearn: boolean
): Promise<void> {
try {
const response = await fetch("/api/evaluations/skills", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
credentials: "same-origin",
body: JSON.stringify({
action: "updateLearningStatus",
category,
skillId,
wantsToLearn,
}),
});
if (!response.ok) {
throw new Error("Failed to update skill learning status");
}
} catch (error) {
console.error("Failed to update skill learning status:", error);
throw error;
}
}
export async function addSkillToEvaluation(
category: string,
skillId: string
): Promise<void> {
try {
const response = await fetch("/api/evaluations/skills", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
credentials: "same-origin",
body: JSON.stringify({
action: "addSkill",
category,
skillId,
}),
});
if (!response.ok) {
throw new Error("Failed to add skill to evaluation");
}
} catch (error) {
console.error("Failed to add skill to evaluation:", error);
throw error;
}
}
export async function removeSkillFromEvaluation(
category: string,
skillId: string
): Promise<void> {
try {
const response = await fetch("/api/evaluations/skills", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
credentials: "same-origin",
body: JSON.stringify({
action: "removeSkill",
category,
skillId,
}),
});
if (!response.ok) {
throw new Error("Failed to remove skill from evaluation");
}
} catch (error) {
console.error("Failed to remove skill from evaluation:", error);
throw error;
}
}
export async function initializeEmptyEvaluation(
profile: import("@/lib/types").UserProfile
): Promise<void> {
try {
// Simplement créer le profil via l'auth, pas besoin de créer une évaluation vide
// Le backend créera automatiquement l'évaluation lors du premier accès
const { AuthService } = await import("@/lib/auth-utils");
await AuthService.login(profile);
} catch (error) {
console.error("Failed to initialize evaluation:", error);
throw error;
}
}