From ffbf3cd42fa4df5f9552340da6a07dec869d8c8b Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Tue, 16 Dec 2025 11:19:54 +0100 Subject: [PATCH] Optimize database calls across multiple components by implementing Promise.all for parallel fetching of data, enhancing performance and reducing loading times. --- app/events/page.tsx | 33 +++-- app/leaderboard/page.tsx | 11 +- app/page.tsx | 9 +- app/profile/page.tsx | 39 +++--- components/feedback/FeedbackModal.tsx | 12 +- components/navigation/ChallengeBadge.tsx | 20 +-- components/navigation/NavigationWrapper.tsx | 26 ++-- services/challenges/challenge.service.ts | 132 ++++++++++++-------- 8 files changed, 162 insertions(+), 120 deletions(-) diff --git a/app/events/page.tsx b/app/events/page.tsx index 54ccc00..524e7bc 100644 --- a/app/events/page.tsx +++ b/app/events/page.tsx @@ -8,9 +8,18 @@ import { auth } from "@/lib/auth"; export const dynamic = "force-dynamic"; export default async function EventsPage() { - const events = await eventService.getAllEvents({ - orderBy: { date: "desc" }, - }); + // Paralléliser les appels indépendants + const session = await auth(); + + const [events, backgroundImage, allRegistrations] = await Promise.all([ + eventService.getAllEvents({ + orderBy: { date: "desc" }, + }), + getBackgroundImage("events", "/got-2.jpg"), + session?.user?.id + ? eventRegistrationService.getUserRegistrations(session.user.id) + : Promise.resolve([]), + ]); // Sérialiser les dates pour le client const serializedEvents = events.map((event) => ({ @@ -20,21 +29,11 @@ export default async function EventsPage() { updatedAt: event.updatedAt.toISOString(), })); - const backgroundImage = await getBackgroundImage("events", "/got-2.jpg"); - - // Récupérer les inscriptions côté serveur pour éviter le clignotement - const session = await auth(); + // Construire le map des inscriptions const initialRegistrations: Record = {}; - - if (session?.user?.id) { - // Récupérer toutes les inscriptions (passées et à venir) pour permettre le feedback - const allRegistrations = - await eventRegistrationService.getUserRegistrations(session.user.id); - - allRegistrations.forEach((reg) => { - initialRegistrations[reg.eventId] = true; - }); - } + allRegistrations.forEach((reg) => { + initialRegistrations[reg.eventId] = true; + }); return (
diff --git a/app/leaderboard/page.tsx b/app/leaderboard/page.tsx index 1df4dbb..49bb417 100644 --- a/app/leaderboard/page.tsx +++ b/app/leaderboard/page.tsx @@ -6,12 +6,11 @@ import { getBackgroundImage } from "@/lib/preferences"; export const dynamic = "force-dynamic"; export default async function LeaderboardPage() { - const leaderboard = await userStatsService.getLeaderboard(10); - - const backgroundImage = await getBackgroundImage( - "leaderboard", - "/leaderboard-bg.jpg" - ); + // Paralléliser les appels DB + const [leaderboard, backgroundImage] = await Promise.all([ + userStatsService.getLeaderboard(10), + getBackgroundImage("leaderboard", "/leaderboard-bg.jpg"), + ]); return (
diff --git a/app/page.tsx b/app/page.tsx index 1a0311e..9be9fcf 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -7,7 +7,11 @@ import { getBackgroundImage } from "@/lib/preferences"; export const dynamic = "force-dynamic"; export default async function Home() { - const events = await eventService.getUpcomingEvents(3); + // Paralléliser les appels DB + const [events, backgroundImage] = await Promise.all([ + eventService.getUpcomingEvents(3), + getBackgroundImage("home", "/got-2.jpg"), + ]); // Convert Date objects to strings for serialization const serializedEvents = events.map((event) => ({ @@ -15,9 +19,6 @@ export default async function Home() { date: event.date.toISOString(), })); - // Récupérer l'image de fond côté serveur - const backgroundImage = await getBackgroundImage("home", "/got-2.jpg"); - return (
diff --git a/app/profile/page.tsx b/app/profile/page.tsx index dbaab1e..8d3e88c 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -12,31 +12,30 @@ export default async function ProfilePage() { redirect("/login"); } - const user = await userService.getUserById(session.user.id, { - id: true, - email: true, - username: true, - avatar: true, - bio: true, - characterClass: true, - hp: true, - maxHp: true, - xp: true, - maxXp: true, - level: true, - score: true, - createdAt: true, - }); + // Paralléliser les appels DB + const [user, backgroundImage] = await Promise.all([ + userService.getUserById(session.user.id, { + id: true, + email: true, + username: true, + avatar: true, + bio: true, + characterClass: true, + hp: true, + maxHp: true, + xp: true, + maxXp: true, + level: true, + score: true, + createdAt: true, + }), + getBackgroundImage("home", "/got-background.jpg"), + ]); if (!user) { redirect("/login"); } - const backgroundImage = await getBackgroundImage( - "home", - "/got-background.jpg" - ); - // Convert Date to string for the component const userProfile = { ...user, diff --git a/components/feedback/FeedbackModal.tsx b/components/feedback/FeedbackModal.tsx index 7396405..e4163e1 100644 --- a/components/feedback/FeedbackModal.tsx +++ b/components/feedback/FeedbackModal.tsx @@ -68,18 +68,22 @@ export default function FeedbackModal({ if (!eventId) return; try { - // Récupérer l'événement - const eventResponse = await fetch(`/api/events/${eventId}`); + // Paralléliser les appels API + const [eventResponse, feedbackResponse] = await Promise.all([ + fetch(`/api/events/${eventId}`), + fetch(`/api/feedback/${eventId}`), + ]); + if (!eventResponse.ok) { setError("Événement introuvable"); setLoading(false); return; } + const eventData = await eventResponse.json(); setEvent(eventData); - // Récupérer le feedback existant si disponible - const feedbackResponse = await fetch(`/api/feedback/${eventId}`); + // Traiter le feedback if (feedbackResponse.ok) { const feedbackData = await feedbackResponse.json(); if (feedbackData.feedback) { diff --git a/components/navigation/ChallengeBadge.tsx b/components/navigation/ChallengeBadge.tsx index 0fd5397..386e0a6 100644 --- a/components/navigation/ChallengeBadge.tsx +++ b/components/navigation/ChallengeBadge.tsx @@ -15,7 +15,13 @@ export default function ChallengeBadge({ const [count, setCount] = useState(initialCount); useEffect(() => { - // Récupérer le nombre de défis actifs + // Si on a déjà un initialCount, l'utiliser et ne pas faire d'appel immédiat + // On rafraîchit seulement après un délai pour éviter les appels redondants + if (initialCount > 0) { + setCount(initialCount); + } + + // Récupérer le nombre de défis actifs (seulement si pas d'initialCount ou pour rafraîchir) const fetchActiveCount = async () => { try { const response = await fetch("/api/challenges/active-count"); @@ -26,13 +32,16 @@ export default function ChallengeBadge({ } }; - fetchActiveCount(); + // Si pas d'initialCount, charger immédiatement, sinon attendre 30s avant le premier refresh + if (initialCount === 0) { + fetchActiveCount(); + } // Rafraîchir toutes les 30 secondes const interval = setInterval(fetchActiveCount, 30000); return () => clearInterval(interval); - }, []); + }, [initialCount]); return ( (e.currentTarget.style.color = "var(--accent-color)") } - onMouseLeave={(e) => - (e.currentTarget.style.color = "var(--foreground)") - } + onMouseLeave={(e) => (e.currentTarget.style.color = "var(--foreground)")} title={ count > 0 ? `${count} défi${count > 1 ? "s" : ""} actif${count > 1 ? "s" : ""}` @@ -69,4 +76,3 @@ export default function ChallengeBadge({ ); } - diff --git a/components/navigation/NavigationWrapper.tsx b/components/navigation/NavigationWrapper.tsx index 1919e2f..bb14e9c 100644 --- a/components/navigation/NavigationWrapper.tsx +++ b/components/navigation/NavigationWrapper.tsx @@ -21,23 +21,25 @@ export default async function NavigationWrapper() { let activeChallengesCount = 0; if (session?.user?.id) { - const user = await userService.getUserById(session.user.id, { - username: true, - avatar: true, - hp: true, - maxHp: true, - xp: true, - maxXp: true, - level: true, - }); + // Paralléliser les appels DB + const [user, count] = await Promise.all([ + userService.getUserById(session.user.id, { + username: true, + avatar: true, + hp: true, + maxHp: true, + xp: true, + maxXp: true, + level: true, + }), + challengeService.getActiveChallengesCount(session.user.id), + ]); if (user) { userData = user; } - // Récupérer le nombre de défis actifs - activeChallengesCount = - await challengeService.getActiveChallengesCount(session.user.id); + activeChallengesCount = count; } return ( diff --git a/services/challenges/challenge.service.ts b/services/challenges/challenge.service.ts index dbdffe2..71c8623 100644 --- a/services/challenges/challenge.service.ts +++ b/services/challenges/challenge.service.ts @@ -165,11 +165,15 @@ export class ChallengeService { winnerId: string, adminComment?: string ): Promise { + // Récupérer uniquement les champs nécessaires pour la validation const challenge = await prisma.challenge.findUnique({ where: { id: challengeId }, - include: { - challenger: true, - challenged: true, + select: { + id: true, + status: true, + challengerId: true, + challengedId: true, + pointsReward: true, }, }); @@ -194,32 +198,49 @@ export class ChallengeService { ); } - // Mettre à jour le défi - const updatedChallenge = await prisma.challenge.update({ - where: { id: challengeId }, - data: { - status: "COMPLETED", - adminId, - adminComment: adminComment || null, - winnerId, - completedAt: new Date(), - }, - include: { - challenger: true, - challenged: true, - winner: true, - }, - }); - - // Attribuer les points au gagnant - await prisma.user.update({ - where: { id: winnerId }, - data: { - score: { - increment: challenge.pointsReward, + // Paralléliser la mise à jour du défi et l'attribution des points + const [updatedChallenge] = await Promise.all([ + prisma.challenge.update({ + where: { id: challengeId }, + data: { + status: "COMPLETED", + adminId, + adminComment: adminComment || null, + winnerId, + completedAt: new Date(), }, - }, - }); + include: { + challenger: { + select: { + id: true, + username: true, + avatar: true, + }, + }, + challenged: { + select: { + id: true, + username: true, + avatar: true, + }, + }, + winner: { + select: { + id: true, + username: true, + }, + }, + }, + }), + prisma.user.update({ + where: { id: winnerId }, + data: { + score: { + increment: challenge.pointsReward, + }, + }, + }), + ]); return updatedChallenge; } @@ -264,14 +285,6 @@ export class ChallengeService { challengeId: string, data: UpdateChallengeInput ): Promise { - const challenge = await prisma.challenge.findUnique({ - where: { id: challengeId }, - }); - - if (!challenge) { - throw new NotFoundError("Défi"); - } - const updateData: Prisma.ChallengeUpdateInput = {}; if (data.title !== undefined) { @@ -300,18 +313,31 @@ export class ChallengeService { : { disconnect: true }; } - return prisma.challenge.update({ - where: { id: challengeId }, - data: updateData, - }); + try { + return await prisma.challenge.update({ + where: { id: challengeId }, + data: updateData, + }); + } catch (error: any) { + if (error?.code === "P2025") { + // Record not found + throw new NotFoundError("Défi"); + } + throw error; + } } /** * Annule un défi (admin seulement) */ async adminCancelChallenge(challengeId: string): Promise { + // Récupérer uniquement le statut pour la validation const challenge = await prisma.challenge.findUnique({ where: { id: challengeId }, + select: { + id: true, + status: true, + }, }); if (!challenge) { @@ -336,8 +362,14 @@ export class ChallengeService { * Remet le défi en PENDING s'il n'avait jamais été accepté, sinon en ACCEPTED */ async reactivateChallenge(challengeId: string): Promise { + // Récupérer uniquement les champs nécessaires const challenge = await prisma.challenge.findUnique({ where: { id: challengeId }, + select: { + id: true, + status: true, + acceptedAt: true, + }, }); if (!challenge) { @@ -367,17 +399,17 @@ export class ChallengeService { * Supprime un défi (admin seulement) */ async deleteChallenge(challengeId: string): Promise { - const challenge = await prisma.challenge.findUnique({ - where: { id: challengeId }, - }); - - if (!challenge) { - throw new NotFoundError("Défi"); + try { + await prisma.challenge.delete({ + where: { id: challengeId }, + }); + } catch (error: any) { + if (error?.code === "P2025") { + // Record not found + throw new NotFoundError("Défi"); + } + throw error; } - - await prisma.challenge.delete({ - where: { id: challengeId }, - }); } /**