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 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" },
});
}),
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<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) => {
initialRegistrations[reg.eventId] = true;
});
}
return (
<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 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 (
<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 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 (
<main className="min-h-screen bg-black relative">
<NavigationWrapper />

View File

@@ -12,7 +12,9 @@ export default async function ProfilePage() {
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,
email: true,
username: true,
@@ -26,17 +28,14 @@ export default async function ProfilePage() {
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,

View File

@@ -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) {

View File

@@ -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({
}
};
// 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 (
<Link
@@ -45,9 +54,7 @@ export default function ChallengeBadge({
onMouseEnter={(e) =>
(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({
</Link>
);
}

View File

@@ -21,7 +21,9 @@ export default async function NavigationWrapper() {
let activeChallengesCount = 0;
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,
avatar: true,
hp: true,
@@ -29,15 +31,15 @@ export default async function NavigationWrapper() {
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 (

View File

@@ -165,11 +165,15 @@ export class ChallengeService {
winnerId: string,
adminComment?: string
): Promise<Challenge> {
// 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,8 +198,9 @@ export class ChallengeService {
);
}
// Mettre à jour le défi
const updatedChallenge = await prisma.challenge.update({
// 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",
@@ -205,21 +210,37 @@ export class ChallengeService {
completedAt: new Date(),
},
include: {
challenger: true,
challenged: true,
winner: true,
challenger: {
select: {
id: true,
username: true,
avatar: true,
},
});
// Attribuer les points au gagnant
await prisma.user.update({
},
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<Challenge> {
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({
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<Challenge> {
// 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<Challenge> {
// 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<void> {
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;
}
}
/**