From ef16c73625d7c35a9edcd83eaf9981a08987303f Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 21 Aug 2025 13:19:46 +0200 Subject: [PATCH] feat: handling SSR on evaluation --- app/api/evaluations/skills/route.ts | 40 ++++-- app/evaluation/page.tsx | 133 +++++------------- components/evaluation/client-wrapper.tsx | 155 +++++++++++++++++++++ components/evaluation/index.ts | 8 +- components/evaluation/welcome-screen.tsx | 73 ++++++++++ components/skill-evaluation.tsx | 38 ++--- lib/evaluation-actions.ts | 169 +++++++++++++++++++++++ 7 files changed, 481 insertions(+), 135 deletions(-) create mode 100644 components/evaluation/client-wrapper.tsx create mode 100644 components/evaluation/welcome-screen.tsx create mode 100644 lib/evaluation-actions.ts diff --git a/app/api/evaluations/skills/route.ts b/app/api/evaluations/skills/route.ts index eaf37ff..9c6ab9d 100644 --- a/app/api/evaluations/skills/route.ts +++ b/app/api/evaluations/skills/route.ts @@ -1,28 +1,40 @@ import { NextRequest, NextResponse } from "next/server"; +import { cookies } from "next/headers"; import { evaluationService } from "@/services/evaluation-service"; import { UserProfile, SkillLevel } from "@/lib/types"; +const COOKIE_NAME = "peakSkills_userId"; + export async function PUT(request: NextRequest) { try { - const body = await request.json(); - const { - profile, - category, - skillId, - level, - canMentor, - wantsToLearn, - action, - } = body; + // Récupérer l'utilisateur depuis le cookie + const cookieStore = await cookies(); + const userId = cookieStore.get(COOKIE_NAME)?.value; - if (!profile || !category || !skillId) { + if (!userId) { return NextResponse.json( - { error: "profile, category et skillId sont requis" }, - { status: 400 } + { error: "Utilisateur non authentifié" }, + { 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) { case "updateLevel": diff --git a/app/evaluation/page.tsx b/app/evaluation/page.tsx index e886f1b..3eefae4 100644 --- a/app/evaluation/page.tsx +++ b/app/evaluation/page.tsx @@ -1,106 +1,49 @@ -"use client"; - -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 { redirect } from "next/navigation"; import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/components/ui/card"; + isUserAuthenticated, + getServerUserEvaluation, + getServerSkillCategories, + getServerTeams, +} from "@/lib/server-auth"; +import { + EvaluationClientWrapper, + WelcomeEvaluationScreen, +} from "@/components/evaluation"; +import { SkillEvaluation } from "@/components/skill-evaluation"; -export default function EvaluationPage() { - const { - userEvaluation, - skillCategories, - teams, - loading, - updateProfile, - updateSkillLevel, - updateSkillMentorStatus, - updateSkillLearningStatus, - addSkillToEvaluation, - removeSkillFromEvaluation, - initializeEmptyEvaluation, - } = useEvaluation(); +export default async function EvaluationPage() { + // Vérifier l'authentification + const isAuthenticated = await isUserAuthenticated(); - const { setUserInfo } = useUser(); - - // 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]); - - if (loading) { - return ( -
-
-
-
-

Chargement...

-
-
-
- ); + // Si pas de cookie d'authentification, rediriger vers login + if (!isAuthenticated) { + redirect("/login"); } - // 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) { - const handleProfileSubmit = (profile: any) => { - initializeEmptyEvaluation(profile); - }; - - return ( -
-
-
- -
-
-

- Commencer l'évaluation -

-

- Renseignez vos informations pour débuter votre auto-évaluation -

-
- -
- -
-
-
- ); + return ; } return ( -
- {/* Skill Evaluation */} - {skillCategories.length > 0 && userEvaluation.evaluations.length > 0 && ( - - )} -
+ +
+ {/* Skill Evaluation */} + {skillCategories.length > 0 && + userEvaluation.evaluations.length > 0 && ( + + )} +
+
); } diff --git a/components/evaluation/client-wrapper.tsx b/components/evaluation/client-wrapper.tsx new file mode 100644 index 0000000..c2b0d69 --- /dev/null +++ b/components/evaluation/client-wrapper.tsx @@ -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 ( + + {children} + + ); +} + +// 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( + undefined +); + +function EvaluationProvider({ + children, + updateSkillLevel, + updateSkillMentorStatus, + updateSkillLearningStatus, + addSkillToEvaluation, + removeSkillFromEvaluation, +}: { + children: React.ReactNode; +} & EvaluationContextType) { + return ( + + {children} + + ); +} + +export function useEvaluationContext() { + const context = useContext(EvaluationContext); + if (context === undefined) { + throw new Error( + "useEvaluationContext must be used within an EvaluationProvider" + ); + } + return context; +} diff --git a/components/evaluation/index.ts b/components/evaluation/index.ts index 3d8026d..f465d0a 100644 --- a/components/evaluation/index.ts +++ b/components/evaluation/index.ts @@ -1,5 +1,9 @@ +export { + EvaluationClientWrapper, + useEvaluationContext, +} from "./client-wrapper"; +export { WelcomeEvaluationScreen } from "./welcome-screen"; export { EvaluationHeader } from "./evaluation-header"; -export { SkillLevelLegend } from "./skill-level-legend"; export { CategoryTabs } from "./category-tabs"; export { SkillEvaluationGrid } from "./skill-evaluation-grid"; -export { SkillEvaluationCard } from "./skill-evaluation-card"; +export { SkillLevelLegend } from "./skill-level-legend"; diff --git a/components/evaluation/welcome-screen.tsx b/components/evaluation/welcome-screen.tsx new file mode 100644 index 0000000..2ab1a55 --- /dev/null +++ b/components/evaluation/welcome-screen.tsx @@ -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 ( +
+
+
+ +
+
+
+ + + PeakSkills - Évaluation + +
+ +

+ Commencer l'évaluation +

+

+ Renseignez vos informations pour débuter votre auto-évaluation +

+
+ +
+
+ {isSubmitting && ( +
+
+
+

+ Initialisation de l'évaluation... +

+
+
+ )} + +
+
+
+
+ ); +} diff --git a/components/skill-evaluation.tsx b/components/skill-evaluation.tsx index 5ee861c..dab8307 100644 --- a/components/skill-evaluation.tsx +++ b/components/skill-evaluation.tsx @@ -5,6 +5,7 @@ import { useSearchParams, useRouter } from "next/navigation"; import { TooltipProvider } from "@/components/ui/tooltip"; import { SkillCategory, SkillLevel, CategoryEvaluation } from "@/lib/types"; import { SkillSelector } from "./skill-selector"; +import { useEvaluationContext } from "./evaluation"; import { EvaluationHeader, SkillLevelLegend, @@ -15,30 +16,19 @@ import { interface SkillEvaluationProps { categories: SkillCategory[]; 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({ categories, evaluations, - onUpdateSkill, - onUpdateMentorStatus, - onUpdateLearningStatus, - onAddSkill, - onRemoveSkill, }: SkillEvaluationProps) { + const { + updateSkillLevel, + updateSkillMentorStatus, + updateSkillLearningStatus, + addSkillToEvaluation, + removeSkillFromEvaluation, + } = useEvaluationContext(); const searchParams = useSearchParams(); const router = useRouter(); const categoryParam = searchParams.get("category"); @@ -97,18 +87,18 @@ export function SkillEvaluation({ categories={categories} evaluations={evaluations} selectedCategory={selectedCategory} - onAddSkill={onAddSkill} - onRemoveSkill={onRemoveSkill} + onAddSkill={addSkillToEvaluation} + onRemoveSkill={removeSkillFromEvaluation} /> {currentEvaluation && ( )}
diff --git a/lib/evaluation-actions.ts b/lib/evaluation-actions.ts new file mode 100644 index 0000000..b5472b6 --- /dev/null +++ b/lib/evaluation-actions.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; + } +}