Add dotenv package for environment variable management and update pnpm-lock.yaml. Adjust layout in RegisterPage and LoginPage components for improved responsiveness. Enhance AdminPanel with ChallengeManagement section and update navigation links for challenges. Refactor Prisma schema to include Challenge model and related enums.

This commit is contained in:
Julien Froidefond
2025-12-15 15:16:54 +01:00
parent f2bb02406e
commit bbb0fbb9a1
34 changed files with 11414 additions and 9081 deletions

View File

@@ -0,0 +1,500 @@
import { prisma } from "../database";
import type {
Challenge,
ChallengeStatus,
Prisma,
} from "@/prisma/generated/prisma/client";
import { ValidationError, NotFoundError, ConflictError } from "../errors";
export interface CreateChallengeInput {
challengerId: string;
challengedId: string;
title: string;
description: string;
pointsReward?: number;
}
export interface UpdateChallengeInput {
status?: ChallengeStatus;
adminId?: string;
adminComment?: string;
winnerId?: string;
title?: string;
description?: string;
pointsReward?: number;
}
export interface ChallengeWithUsers extends Challenge {
challenger: {
id: string;
username: string;
avatar: string | null;
};
challenged: {
id: string;
username: string;
avatar: string | null;
};
admin?: {
id: string;
username: string;
} | null;
winner?: {
id: string;
username: string;
} | null;
}
/**
* Service de gestion des défis entre joueurs
*/
export class ChallengeService {
/**
* Crée un nouveau défi
*/
async createChallenge(
data: CreateChallengeInput
): Promise<Challenge> {
// Vérifier que les deux joueurs existent
const [challenger, challenged] = await Promise.all([
prisma.user.findUnique({ where: { id: data.challengerId } }),
prisma.user.findUnique({ where: { id: data.challengedId } }),
]);
if (!challenger) {
throw new NotFoundError("Joueur qui lance le défi");
}
if (!challenged) {
throw new NotFoundError("Joueur qui reçoit le défi");
}
// Vérifier qu'on ne se défie pas soi-même
if (data.challengerId === data.challengedId) {
throw new ValidationError("Vous ne pouvez pas vous défier vous-même");
}
return prisma.challenge.create({
data: {
challengerId: data.challengerId,
challengedId: data.challengedId,
title: data.title,
description: data.description,
pointsReward: data.pointsReward || 100,
status: "PENDING",
},
});
}
/**
* Accepte un défi
*/
async acceptChallenge(
challengeId: string,
userId: string
): Promise<Challenge> {
const challenge = await prisma.challenge.findUnique({
where: { id: challengeId },
});
if (!challenge) {
throw new NotFoundError("Défi");
}
// Vérifier que l'utilisateur est bien celui qui reçoit le défi
if (challenge.challengedId !== userId) {
throw new ValidationError(
"Vous n'êtes pas autorisé à accepter ce défi"
);
}
// Vérifier que le défi est en attente
if (challenge.status !== "PENDING") {
throw new ValidationError(
"Ce défi ne peut plus être accepté (statut: " + challenge.status + ")"
);
}
return prisma.challenge.update({
where: { id: challengeId },
data: {
status: "ACCEPTED",
acceptedAt: new Date(),
},
});
}
/**
* Annule un défi (par le challenger ou le challenged)
*/
async cancelChallenge(
challengeId: string,
userId: string
): Promise<Challenge> {
const challenge = await prisma.challenge.findUnique({
where: { id: challengeId },
});
if (!challenge) {
throw new NotFoundError("Défi");
}
// Vérifier que l'utilisateur est bien impliqué dans le défi
if (
challenge.challengerId !== userId &&
challenge.challengedId !== userId
) {
throw new ValidationError(
"Vous n'êtes pas autorisé à annuler ce défi"
);
}
// Vérifier que le défi peut être annulé
if (challenge.status === "COMPLETED") {
throw new ValidationError("Un défi complété ne peut pas être annulé");
}
return prisma.challenge.update({
where: { id: challengeId },
data: {
status: "CANCELLED",
},
});
}
/**
* Valide un défi (admin seulement)
*/
async validateChallenge(
challengeId: string,
adminId: string,
winnerId: string,
adminComment?: string
): Promise<Challenge> {
const challenge = await prisma.challenge.findUnique({
where: { id: challengeId },
include: {
challenger: true,
challenged: true,
},
});
if (!challenge) {
throw new NotFoundError("Défi");
}
// Vérifier que le défi est accepté
if (challenge.status !== "ACCEPTED") {
throw new ValidationError(
"Seuls les défis acceptés peuvent être validés"
);
}
// Vérifier que le winner est bien l'un des deux joueurs
if (
winnerId !== challenge.challengerId &&
winnerId !== challenge.challengedId
) {
throw new ValidationError(
"Le gagnant doit être l'un des deux joueurs du défi"
);
}
// 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,
},
},
});
return updatedChallenge;
}
/**
* Rejette un défi (admin seulement)
*/
async rejectChallenge(
challengeId: string,
adminId: string,
adminComment?: string
): Promise<Challenge> {
const challenge = await prisma.challenge.findUnique({
where: { id: challengeId },
});
if (!challenge) {
throw new NotFoundError("Défi");
}
// Vérifier que le défi est accepté
if (challenge.status !== "ACCEPTED") {
throw new ValidationError(
"Seuls les défis acceptés peuvent être rejetés"
);
}
return prisma.challenge.update({
where: { id: challengeId },
data: {
status: "REJECTED",
adminId,
adminComment: adminComment || null,
},
});
}
/**
* Met à jour un défi (admin seulement)
*/
async updateChallenge(
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) {
updateData.title = data.title;
}
if (data.description !== undefined) {
updateData.description = data.description;
}
if (data.pointsReward !== undefined) {
updateData.pointsReward = data.pointsReward;
}
if (data.status !== undefined) {
updateData.status = data.status;
}
if (data.adminId !== undefined) {
updateData.adminId = data.adminId;
}
if (data.adminComment !== undefined) {
updateData.adminComment = data.adminComment;
}
if (data.winnerId !== undefined) {
updateData.winnerId = data.winnerId;
}
return prisma.challenge.update({
where: { id: challengeId },
data: updateData,
});
}
/**
* 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");
}
await prisma.challenge.delete({
where: { id: challengeId },
});
}
/**
* Récupère un défi par son ID avec les utilisateurs
*/
async getChallengeById(id: string): Promise<ChallengeWithUsers | null> {
const challenge = await prisma.challenge.findUnique({
where: { id },
include: {
challenger: {
select: {
id: true,
username: true,
avatar: true,
},
},
challenged: {
select: {
id: true,
username: true,
avatar: true,
},
},
admin: {
select: {
id: true,
username: true,
},
},
winner: {
select: {
id: true,
username: true,
},
},
},
});
return challenge as ChallengeWithUsers | null;
}
/**
* Récupère tous les défis d'un utilisateur
*/
async getUserChallenges(userId: string): Promise<ChallengeWithUsers[]> {
return prisma.challenge.findMany({
where: {
OR: [
{ challengerId: userId },
{ challengedId: userId },
],
},
include: {
challenger: {
select: {
id: true,
username: true,
avatar: true,
},
},
challenged: {
select: {
id: true,
username: true,
avatar: true,
},
},
admin: {
select: {
id: true,
username: true,
},
},
winner: {
select: {
id: true,
username: true,
},
},
},
orderBy: {
createdAt: "desc",
},
}) as Promise<ChallengeWithUsers[]>;
}
/**
* Récupère les défis en attente de validation admin
*/
async getPendingValidationChallenges(): Promise<ChallengeWithUsers[]> {
return prisma.challenge.findMany({
where: {
status: "ACCEPTED",
},
include: {
challenger: {
select: {
id: true,
username: true,
avatar: true,
},
},
challenged: {
select: {
id: true,
username: true,
avatar: true,
},
},
admin: {
select: {
id: true,
username: true,
},
},
winner: {
select: {
id: true,
username: true,
},
},
},
orderBy: {
acceptedAt: "asc",
},
}) as Promise<ChallengeWithUsers[]>;
}
/**
* Récupère tous les défis (pour admin)
*/
async getAllChallenges(options?: {
status?: ChallengeStatus;
take?: number;
}): Promise<ChallengeWithUsers[]> {
return prisma.challenge.findMany({
where: options?.status ? { status: options.status } : undefined,
include: {
challenger: {
select: {
id: true,
username: true,
avatar: true,
},
},
challenged: {
select: {
id: true,
username: true,
avatar: true,
},
},
admin: {
select: {
id: true,
username: true,
},
},
winner: {
select: {
id: true,
username: true,
},
},
},
orderBy: {
createdAt: "desc",
},
take: options?.take,
}) as Promise<ChallengeWithUsers[]>;
}
}
export const challengeService = new ChallengeService();