Optimize database calls across multiple components by implementing Promise.all for parallel fetching of data, enhancing performance and reducing loading times.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m40s

This commit is contained in:
Julien Froidefond
2025-12-16 11:19:54 +01:00
parent a9a4120874
commit ffbf3cd42f
8 changed files with 162 additions and 120 deletions

View File

@@ -8,9 +8,18 @@ import { auth } from "@/lib/auth";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export default async function EventsPage() { export default async function EventsPage() {
const events = await eventService.getAllEvents({ // Paralléliser les appels indépendants
const session = await auth();
const [events, backgroundImage, allRegistrations] = await Promise.all([
eventService.getAllEvents({
orderBy: { date: "desc" }, 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 // Sérialiser les dates pour le client
const serializedEvents = events.map((event) => ({ const serializedEvents = events.map((event) => ({
@@ -20,21 +29,11 @@ export default async function EventsPage() {
updatedAt: event.updatedAt.toISOString(), updatedAt: event.updatedAt.toISOString(),
})); }));
const backgroundImage = await getBackgroundImage("events", "/got-2.jpg"); // Construire le map des inscriptions
// Récupérer les inscriptions côté serveur pour éviter le clignotement
const session = await auth();
const initialRegistrations: Record<string, boolean> = {}; const initialRegistrations: Record<string, boolean> = {};
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) => { allRegistrations.forEach((reg) => {
initialRegistrations[reg.eventId] = true; initialRegistrations[reg.eventId] = true;
}); });
}
return ( return (
<main className="min-h-screen bg-black relative"> <main className="min-h-screen bg-black relative">

View File

@@ -6,12 +6,11 @@ import { getBackgroundImage } from "@/lib/preferences";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export default async function LeaderboardPage() { export default async function LeaderboardPage() {
const leaderboard = await userStatsService.getLeaderboard(10); // Paralléliser les appels DB
const [leaderboard, backgroundImage] = await Promise.all([
const backgroundImage = await getBackgroundImage( userStatsService.getLeaderboard(10),
"leaderboard", getBackgroundImage("leaderboard", "/leaderboard-bg.jpg"),
"/leaderboard-bg.jpg" ]);
);
return ( return (
<main className="min-h-screen bg-black relative"> <main className="min-h-screen bg-black relative">

View File

@@ -7,7 +7,11 @@ import { getBackgroundImage } from "@/lib/preferences";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
export default async function Home() { 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 // Convert Date objects to strings for serialization
const serializedEvents = events.map((event) => ({ const serializedEvents = events.map((event) => ({
@@ -15,9 +19,6 @@ export default async function Home() {
date: event.date.toISOString(), date: event.date.toISOString(),
})); }));
// Récupérer l'image de fond côté serveur
const backgroundImage = await getBackgroundImage("home", "/got-2.jpg");
return ( return (
<main className="min-h-screen bg-black relative"> <main className="min-h-screen bg-black relative">
<NavigationWrapper /> <NavigationWrapper />

View File

@@ -12,7 +12,9 @@ export default async function ProfilePage() {
redirect("/login"); redirect("/login");
} }
const user = await userService.getUserById(session.user.id, { // Paralléliser les appels DB
const [user, backgroundImage] = await Promise.all([
userService.getUserById(session.user.id, {
id: true, id: true,
email: true, email: true,
username: true, username: true,
@@ -26,17 +28,14 @@ export default async function ProfilePage() {
level: true, level: true,
score: true, score: true,
createdAt: true, createdAt: true,
}); }),
getBackgroundImage("home", "/got-background.jpg"),
]);
if (!user) { if (!user) {
redirect("/login"); redirect("/login");
} }
const backgroundImage = await getBackgroundImage(
"home",
"/got-background.jpg"
);
// Convert Date to string for the component // Convert Date to string for the component
const userProfile = { const userProfile = {
...user, ...user,

View File

@@ -68,18 +68,22 @@ export default function FeedbackModal({
if (!eventId) return; if (!eventId) return;
try { try {
// Récupérer l'événement // Paralléliser les appels API
const eventResponse = await fetch(`/api/events/${eventId}`); const [eventResponse, feedbackResponse] = await Promise.all([
fetch(`/api/events/${eventId}`),
fetch(`/api/feedback/${eventId}`),
]);
if (!eventResponse.ok) { if (!eventResponse.ok) {
setError("Événement introuvable"); setError("Événement introuvable");
setLoading(false); setLoading(false);
return; return;
} }
const eventData = await eventResponse.json(); const eventData = await eventResponse.json();
setEvent(eventData); setEvent(eventData);
// Récupérer le feedback existant si disponible // Traiter le feedback
const feedbackResponse = await fetch(`/api/feedback/${eventId}`);
if (feedbackResponse.ok) { if (feedbackResponse.ok) {
const feedbackData = await feedbackResponse.json(); const feedbackData = await feedbackResponse.json();
if (feedbackData.feedback) { if (feedbackData.feedback) {

View File

@@ -15,7 +15,13 @@ export default function ChallengeBadge({
const [count, setCount] = useState(initialCount); const [count, setCount] = useState(initialCount);
useEffect(() => { 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 () => { const fetchActiveCount = async () => {
try { try {
const response = await fetch("/api/challenges/active-count"); const response = await fetch("/api/challenges/active-count");
@@ -26,13 +32,16 @@ export default function ChallengeBadge({
} }
}; };
// Si pas d'initialCount, charger immédiatement, sinon attendre 30s avant le premier refresh
if (initialCount === 0) {
fetchActiveCount(); fetchActiveCount();
}
// Rafraîchir toutes les 30 secondes // Rafraîchir toutes les 30 secondes
const interval = setInterval(fetchActiveCount, 30000); const interval = setInterval(fetchActiveCount, 30000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, []); }, [initialCount]);
return ( return (
<Link <Link
@@ -45,9 +54,7 @@ export default function ChallengeBadge({
onMouseEnter={(e) => onMouseEnter={(e) =>
(e.currentTarget.style.color = "var(--accent-color)") (e.currentTarget.style.color = "var(--accent-color)")
} }
onMouseLeave={(e) => onMouseLeave={(e) => (e.currentTarget.style.color = "var(--foreground)")}
(e.currentTarget.style.color = "var(--foreground)")
}
title={ title={
count > 0 count > 0
? `${count} défi${count > 1 ? "s" : ""} actif${count > 1 ? "s" : ""}` ? `${count} défi${count > 1 ? "s" : ""} actif${count > 1 ? "s" : ""}`
@@ -69,4 +76,3 @@ export default function ChallengeBadge({
</Link> </Link>
); );
} }

View File

@@ -21,7 +21,9 @@ export default async function NavigationWrapper() {
let activeChallengesCount = 0; let activeChallengesCount = 0;
if (session?.user?.id) { if (session?.user?.id) {
const user = await userService.getUserById(session.user.id, { // Paralléliser les appels DB
const [user, count] = await Promise.all([
userService.getUserById(session.user.id, {
username: true, username: true,
avatar: true, avatar: true,
hp: true, hp: true,
@@ -29,15 +31,15 @@ export default async function NavigationWrapper() {
xp: true, xp: true,
maxXp: true, maxXp: true,
level: true, level: true,
}); }),
challengeService.getActiveChallengesCount(session.user.id),
]);
if (user) { if (user) {
userData = user; userData = user;
} }
// Récupérer le nombre de défis actifs activeChallengesCount = count;
activeChallengesCount =
await challengeService.getActiveChallengesCount(session.user.id);
} }
return ( return (

View File

@@ -165,11 +165,15 @@ export class ChallengeService {
winnerId: string, winnerId: string,
adminComment?: string adminComment?: string
): Promise<Challenge> { ): Promise<Challenge> {
// Récupérer uniquement les champs nécessaires pour la validation
const challenge = await prisma.challenge.findUnique({ const challenge = await prisma.challenge.findUnique({
where: { id: challengeId }, where: { id: challengeId },
include: { select: {
challenger: true, id: true,
challenged: true, status: true,
challengerId: true,
challengedId: true,
pointsReward: true,
}, },
}); });
@@ -194,8 +198,9 @@ export class ChallengeService {
); );
} }
// Mettre à jour le défi // Paralléliser la mise à jour du défi et l'attribution des points
const updatedChallenge = await prisma.challenge.update({ const [updatedChallenge] = await Promise.all([
prisma.challenge.update({
where: { id: challengeId }, where: { id: challengeId },
data: { data: {
status: "COMPLETED", status: "COMPLETED",
@@ -205,21 +210,37 @@ export class ChallengeService {
completedAt: new Date(), completedAt: new Date(),
}, },
include: { include: {
challenger: true, challenger: {
challenged: true, select: {
winner: true, id: true,
username: true,
avatar: true,
}, },
}); },
challenged: {
// Attribuer les points au gagnant select: {
await prisma.user.update({ id: true,
username: true,
avatar: true,
},
},
winner: {
select: {
id: true,
username: true,
},
},
},
}),
prisma.user.update({
where: { id: winnerId }, where: { id: winnerId },
data: { data: {
score: { score: {
increment: challenge.pointsReward, increment: challenge.pointsReward,
}, },
}, },
}); }),
]);
return updatedChallenge; return updatedChallenge;
} }
@@ -264,14 +285,6 @@ export class ChallengeService {
challengeId: string, challengeId: string,
data: UpdateChallengeInput data: UpdateChallengeInput
): Promise<Challenge> { ): Promise<Challenge> {
const challenge = await prisma.challenge.findUnique({
where: { id: challengeId },
});
if (!challenge) {
throw new NotFoundError("Défi");
}
const updateData: Prisma.ChallengeUpdateInput = {}; const updateData: Prisma.ChallengeUpdateInput = {};
if (data.title !== undefined) { if (data.title !== undefined) {
@@ -300,18 +313,31 @@ export class ChallengeService {
: { disconnect: true }; : { disconnect: true };
} }
return prisma.challenge.update({ try {
return await prisma.challenge.update({
where: { id: challengeId }, where: { id: challengeId },
data: updateData, 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) * Annule un défi (admin seulement)
*/ */
async adminCancelChallenge(challengeId: string): Promise<Challenge> { async adminCancelChallenge(challengeId: string): Promise<Challenge> {
// Récupérer uniquement le statut pour la validation
const challenge = await prisma.challenge.findUnique({ const challenge = await prisma.challenge.findUnique({
where: { id: challengeId }, where: { id: challengeId },
select: {
id: true,
status: true,
},
}); });
if (!challenge) { 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 * Remet le défi en PENDING s'il n'avait jamais été accepté, sinon en ACCEPTED
*/ */
async reactivateChallenge(challengeId: string): Promise<Challenge> { async reactivateChallenge(challengeId: string): Promise<Challenge> {
// Récupérer uniquement les champs nécessaires
const challenge = await prisma.challenge.findUnique({ const challenge = await prisma.challenge.findUnique({
where: { id: challengeId }, where: { id: challengeId },
select: {
id: true,
status: true,
acceptedAt: true,
},
}); });
if (!challenge) { if (!challenge) {
@@ -367,17 +399,17 @@ export class ChallengeService {
* Supprime un défi (admin seulement) * Supprime un défi (admin seulement)
*/ */
async deleteChallenge(challengeId: string): Promise<void> { async deleteChallenge(challengeId: string): Promise<void> {
const challenge = await prisma.challenge.findUnique({ try {
where: { id: challengeId },
});
if (!challenge) {
throw new NotFoundError("Défi");
}
await prisma.challenge.delete({ await prisma.challenge.delete({
where: { id: challengeId }, where: { id: challengeId },
}); });
} catch (error: any) {
if (error?.code === "P2025") {
// Record not found
throw new NotFoundError("Défi");
}
throw error;
}
} }
/** /**