Refactor challenge management functions and improve code formatting: Standardize import statements, enhance error handling messages, and apply consistent formatting across challenge validation, rejection, update, and deletion functions for better readability and maintainability.

This commit is contained in:
Julien Froidefond
2025-12-15 21:20:13 +01:00
parent e4b0907801
commit 321da3176e
4 changed files with 164 additions and 103 deletions

View File

@@ -1,20 +1,17 @@
'use server' "use server";
import { revalidatePath } from 'next/cache' import { revalidatePath } from "next/cache";
import { auth } from '@/lib/auth' import { auth } from "@/lib/auth";
import { challengeService } from '@/services/challenges/challenge.service' import { challengeService } from "@/services/challenges/challenge.service";
import { Role } from '@/prisma/generated/prisma/client' import { Role } from "@/prisma/generated/prisma/client";
import { import { ValidationError, NotFoundError } from "@/services/errors";
ValidationError,
NotFoundError,
} from '@/services/errors'
async function checkAdminAccess() { async function checkAdminAccess() {
const session = await auth() const session = await auth();
if (!session?.user || session.user.role !== Role.ADMIN) { if (!session?.user || session.user.role !== Role.ADMIN) {
throw new Error('Accès refusé - Admin uniquement') throw new Error("Accès refusé - Admin uniquement");
} }
return session return session;
} }
export async function validateChallenge( export async function validateChallenge(
@@ -23,34 +20,41 @@ export async function validateChallenge(
adminComment?: string adminComment?: string
) { ) {
try { try {
const session = await checkAdminAccess() const session = await checkAdminAccess();
const challenge = await challengeService.validateChallenge( const challenge = await challengeService.validateChallenge(
challengeId, challengeId,
session.user.id, session.user.id,
winnerId, winnerId,
adminComment adminComment
) );
revalidatePath('/admin') revalidatePath("/admin");
revalidatePath('/challenges') revalidatePath("/challenges");
revalidatePath('/leaderboard') revalidatePath("/leaderboard");
return { success: true, message: 'Défi validé avec succès', data: challenge } return {
success: true,
message: "Défi validé avec succès",
data: challenge,
};
} catch (error) { } catch (error) {
console.error('Validate challenge error:', error) console.error("Validate challenge error:", error);
if (error instanceof ValidationError) { if (error instanceof ValidationError) {
return { success: false, error: error.message } return { success: false, error: error.message };
} }
if (error instanceof NotFoundError) { if (error instanceof NotFoundError) {
return { success: false, error: error.message } return { success: false, error: error.message };
} }
if (error instanceof Error && error.message.includes('Accès refusé')) { if (error instanceof Error && error.message.includes("Accès refusé")) {
return { success: false, error: error.message } return { success: false, error: error.message };
} }
return { success: false, error: 'Une erreur est survenue lors de la validation du défi' } return {
success: false,
error: "Une erreur est survenue lors de la validation du défi",
};
} }
} }
@@ -59,94 +63,106 @@ export async function rejectChallenge(
adminComment?: string adminComment?: string
) { ) {
try { try {
const session = await checkAdminAccess() const session = await checkAdminAccess();
const challenge = await challengeService.rejectChallenge( const challenge = await challengeService.rejectChallenge(
challengeId, challengeId,
session.user.id, session.user.id,
adminComment adminComment
) );
revalidatePath('/admin') revalidatePath("/admin");
revalidatePath('/challenges') revalidatePath("/challenges");
return { success: true, message: 'Défi rejeté', data: challenge } return { success: true, message: "Défi rejeté", data: challenge };
} catch (error) { } catch (error) {
console.error('Reject challenge error:', error) console.error("Reject challenge error:", error);
if (error instanceof ValidationError) { if (error instanceof ValidationError) {
return { success: false, error: error.message } return { success: false, error: error.message };
} }
if (error instanceof NotFoundError) { if (error instanceof NotFoundError) {
return { success: false, error: error.message } return { success: false, error: error.message };
} }
if (error instanceof Error && error.message.includes('Accès refusé')) { if (error instanceof Error && error.message.includes("Accès refusé")) {
return { success: false, error: error.message } return { success: false, error: error.message };
} }
return { success: false, error: 'Une erreur est survenue lors du rejet du défi' } return {
success: false,
error: "Une erreur est survenue lors du rejet du défi",
};
} }
} }
export async function updateChallenge( export async function updateChallenge(
challengeId: string, challengeId: string,
data: { data: {
title?: string title?: string;
description?: string description?: string;
pointsReward?: number pointsReward?: number;
} }
) { ) {
try { try {
await checkAdminAccess() await checkAdminAccess();
const challenge = await challengeService.updateChallenge(challengeId, { const challenge = await challengeService.updateChallenge(challengeId, {
title: data.title, title: data.title,
description: data.description, description: data.description,
pointsReward: data.pointsReward, pointsReward: data.pointsReward,
}) });
revalidatePath('/admin') revalidatePath("/admin");
revalidatePath('/challenges') revalidatePath("/challenges");
return { success: true, message: 'Défi mis à jour avec succès', data: challenge } return {
success: true,
message: "Défi mis à jour avec succès",
data: challenge,
};
} catch (error) { } catch (error) {
console.error('Update challenge error:', error) console.error("Update challenge error:", error);
if (error instanceof ValidationError) { if (error instanceof ValidationError) {
return { success: false, error: error.message } return { success: false, error: error.message };
} }
if (error instanceof NotFoundError) { if (error instanceof NotFoundError) {
return { success: false, error: error.message } return { success: false, error: error.message };
} }
if (error instanceof Error && error.message.includes('Accès refusé')) { if (error instanceof Error && error.message.includes("Accès refusé")) {
return { success: false, error: error.message } return { success: false, error: error.message };
} }
return { success: false, error: 'Une erreur est survenue lors de la mise à jour du défi' } return {
success: false,
error: "Une erreur est survenue lors de la mise à jour du défi",
};
} }
} }
export async function deleteChallenge(challengeId: string) { export async function deleteChallenge(challengeId: string) {
try { try {
await checkAdminAccess() await checkAdminAccess();
await challengeService.deleteChallenge(challengeId) await challengeService.deleteChallenge(challengeId);
revalidatePath('/admin') revalidatePath("/admin");
revalidatePath('/challenges') revalidatePath("/challenges");
return { success: true, message: 'Défi supprimé avec succès' } return { success: true, message: "Défi supprimé avec succès" };
} catch (error) { } catch (error) {
console.error('Delete challenge error:', error) console.error("Delete challenge error:", error);
if (error instanceof NotFoundError) { if (error instanceof NotFoundError) {
return { success: false, error: error.message } return { success: false, error: error.message };
} }
if (error instanceof Error && error.message.includes('Accès refusé')) { if (error instanceof Error && error.message.includes("Accès refusé")) {
return { success: false, error: error.message } return { success: false, error: error.message };
} }
return { success: false, error: 'Une erreur est survenue lors de la suppression du défi' } return {
success: false,
error: "Une erreur est survenue lors de la suppression du défi",
};
} }
} }

View File

@@ -1,7 +1,12 @@
"use client"; "use client";
import { useEffect, useState, useTransition } from "react"; import { useEffect, useState, useTransition } from "react";
import { validateChallenge, rejectChallenge, updateChallenge, deleteChallenge } from "@/actions/admin/challenges"; import {
validateChallenge,
rejectChallenge,
updateChallenge,
deleteChallenge,
} from "@/actions/admin/challenges";
import { Button, Card, Input, Textarea, Alert } from "@/components/ui"; import { Button, Card, Input, Textarea, Alert } from "@/components/ui";
import { Avatar } from "@/components/ui"; import { Avatar } from "@/components/ui";
@@ -29,8 +34,12 @@ interface Challenge {
export default function ChallengeManagement() { export default function ChallengeManagement() {
const [challenges, setChallenges] = useState<Challenge[]>([]); const [challenges, setChallenges] = useState<Challenge[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [selectedChallenge, setSelectedChallenge] = useState<Challenge | null>(null); const [selectedChallenge, setSelectedChallenge] = useState<Challenge | null>(
const [editingChallenge, setEditingChallenge] = useState<Challenge | null>(null); null
);
const [editingChallenge, setEditingChallenge] = useState<Challenge | null>(
null
);
const [winnerId, setWinnerId] = useState<string>(""); const [winnerId, setWinnerId] = useState<string>("");
const [adminComment, setAdminComment] = useState(""); const [adminComment, setAdminComment] = useState("");
const [editTitle, setEditTitle] = useState(""); const [editTitle, setEditTitle] = useState("");
@@ -73,7 +82,9 @@ export default function ChallengeManagement() {
); );
if (result.success) { if (result.success) {
setSuccessMessage("Défi validé avec succès ! Les points ont été attribués."); setSuccessMessage(
"Défi validé avec succès ! Les points ont été attribués."
);
setSelectedChallenge(null); setSelectedChallenge(null);
setWinnerId(""); setWinnerId("");
setAdminComment(""); setAdminComment("");
@@ -145,7 +156,11 @@ export default function ChallengeManagement() {
}; };
const handleDelete = async (challengeId: string) => { const handleDelete = async (challengeId: string) => {
if (!confirm("Êtes-vous sûr de vouloir supprimer ce défi ? Cette action est irréversible.")) { if (
!confirm(
"Êtes-vous sûr de vouloir supprimer ce défi ? Cette action est irréversible."
)
) {
return; return;
} }
@@ -193,10 +208,12 @@ export default function ChallengeManagement() {
</Alert> </Alert>
)} )}
<div className="text-sm text-gray-400 mb-4"> <div className="text-sm text-gray-400 mb-4">
{acceptedChallenges.length} défi{acceptedChallenges.length > 1 ? "s" : ""} en attente de validation {acceptedChallenges.length} défi
{acceptedChallenges.length > 1 ? "s" : ""} en attente de validation
{pendingChallenges.length > 0 && ( {pendingChallenges.length > 0 && (
<span className="ml-2"> <span className="ml-2">
{pendingChallenges.length} défi{pendingChallenges.length > 1 ? "s" : ""} en attente d&apos;acceptation {pendingChallenges.length} défi
{pendingChallenges.length > 1 ? "s" : ""} en attente d'acceptation
</span> </span>
)} )}
</div> </div>
@@ -233,20 +250,28 @@ export default function ChallengeManagement() {
</div> </div>
<div className="text-sm text-gray-400"> <div className="text-sm text-gray-400">
Récompense: <span className="text-pixel-gold font-bold">{challenge.pointsReward} points</span> Récompense:{" "}
<span className="text-pixel-gold font-bold">
{challenge.pointsReward} points
</span>
</div> </div>
<div className="text-xs mt-2"> <div className="text-xs mt-2">
<span className={`px-2 py-1 rounded ${ <span
className={`px-2 py-1 rounded ${
challenge.status === "ACCEPTED" challenge.status === "ACCEPTED"
? "bg-green-500/20 text-green-400" ? "bg-green-500/20 text-green-400"
: "bg-yellow-500/20 text-yellow-400" : "bg-yellow-500/20 text-yellow-400"
}`}> }`}
{challenge.status === "ACCEPTED" ? "Accepté" : "En attente d&apos;acceptation"} >
{challenge.status === "ACCEPTED"
? "Accepté"
: "En attente d'acceptation"}
</span> </span>
</div> </div>
{challenge.acceptedAt && ( {challenge.acceptedAt && (
<div className="text-xs text-gray-500 mt-2"> <div className="text-xs text-gray-500 mt-2">
Accepté le: {new Date(challenge.acceptedAt).toLocaleDateString("fr-FR")} Accepté le:{" "}
{new Date(challenge.acceptedAt).toLocaleDateString("fr-FR")}
</div> </div>
)} )}
</div> </div>
@@ -466,7 +491,9 @@ export default function ChallengeManagement() {
type="number" type="number"
min="1" min="1"
value={editPointsReward} value={editPointsReward}
onChange={(e) => setEditPointsReward(parseInt(e.target.value) || 0)} onChange={(e) =>
setEditPointsReward(parseInt(e.target.value) || 0)
}
required required
placeholder="100" placeholder="100"
/> />
@@ -475,7 +502,12 @@ export default function ChallengeManagement() {
<Button <Button
onClick={handleUpdate} onClick={handleUpdate}
variant="primary" variant="primary"
disabled={isPending || !editTitle || !editDescription || editPointsReward <= 0} disabled={
isPending ||
!editTitle ||
!editDescription ||
editPointsReward <= 0
}
className="flex-1" className="flex-1"
> >
{isPending ? "Mise à jour..." : "Enregistrer"} {isPending ? "Mise à jour..." : "Enregistrer"}
@@ -501,4 +533,3 @@ export default function ChallengeManagement() {
</div> </div>
); );
} }

View File

@@ -2,8 +2,19 @@
import { useEffect, useState, useTransition } from "react"; import { useEffect, useState, useTransition } from "react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { createChallenge, acceptChallenge, cancelChallenge } from "@/actions/challenges/create"; import {
import { Button, Card, SectionTitle, Input, Textarea, Alert } from "@/components/ui"; createChallenge,
acceptChallenge,
cancelChallenge,
} from "@/actions/challenges/create";
import {
Button,
Card,
SectionTitle,
Input,
Textarea,
Alert,
} from "@/components/ui";
import { Avatar } from "@/components/ui"; import { Avatar } from "@/components/ui";
interface User { interface User {
@@ -44,7 +55,9 @@ interface ChallengesSectionProps {
backgroundImage: string; backgroundImage: string;
} }
export default function ChallengesSection({ backgroundImage }: ChallengesSectionProps) { export default function ChallengesSection({
backgroundImage,
}: ChallengesSectionProps) {
const { data: session } = useSession(); const { data: session } = useSession();
const [challenges, setChallenges] = useState<Challenge[]>([]); const [challenges, setChallenges] = useState<Challenge[]>([]);
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);
@@ -159,7 +172,7 @@ export default function ChallengesSection({ backgroundImage }: ChallengesSection
const getStatusLabel = (status: string) => { const getStatusLabel = (status: string) => {
switch (status) { switch (status) {
case "PENDING": case "PENDING":
return "En attente d&apos;acceptation"; return "En attente d'acceptation";
case "ACCEPTED": case "ACCEPTED":
return "Accepté - En attente de validation admin"; return "Accepté - En attente de validation admin";
case "COMPLETED": case "COMPLETED":
@@ -284,7 +297,9 @@ export default function ChallengesSection({ backgroundImage }: ChallengesSection
<Input <Input
type="number" type="number"
value={pointsReward} value={pointsReward}
onChange={(e) => setPointsReward(parseInt(e.target.value) || 100)} onChange={(e) =>
setPointsReward(parseInt(e.target.value) || 100)
}
min={1} min={1}
max={1000} max={1000}
/> />
@@ -319,7 +334,8 @@ export default function ChallengesSection({ backgroundImage }: ChallengesSection
const isChallenged = challenge.challenged.id === currentUserId; const isChallenged = challenge.challenged.id === currentUserId;
const canAccept = challenge.status === "PENDING" && isChallenged; const canAccept = challenge.status === "PENDING" && isChallenged;
const canCancel = const canCancel =
(challenge.status === "PENDING" || challenge.status === "ACCEPTED") && (challenge.status === "PENDING" ||
challenge.status === "ACCEPTED") &&
(isChallenger || isChallenged); (isChallenger || isChallenged);
return ( return (
@@ -339,7 +355,9 @@ export default function ChallengesSection({ backgroundImage }: ChallengesSection
</span> </span>
</div> </div>
<p className="text-gray-300 mb-4">{challenge.description}</p> <p className="text-gray-300 mb-4">
{challenge.description}
</p>
<div className="flex items-center gap-4 mb-2"> <div className="flex items-center gap-4 mb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -385,7 +403,10 @@ export default function ChallengesSection({ backgroundImage }: ChallengesSection
)} )}
<div className="text-xs text-gray-500 mt-2"> <div className="text-xs text-gray-500 mt-2">
Créé le: {new Date(challenge.createdAt).toLocaleDateString("fr-FR")} Créé le:{" "}
{new Date(challenge.createdAt).toLocaleDateString(
"fr-FR"
)}
{challenge.acceptedAt && {challenge.acceptedAt &&
` • Accepté le: ${new Date(challenge.acceptedAt).toLocaleDateString("fr-FR")}`} ` • Accepté le: ${new Date(challenge.acceptedAt).toLocaleDateString("fr-FR")}`}
{challenge.completedAt && {challenge.completedAt &&
@@ -425,4 +446,3 @@ export default function ChallengesSection({ backgroundImage }: ChallengesSection
</section> </section>
); );
} }

View File

@@ -52,9 +52,7 @@ export class ChallengeService {
/** /**
* Crée un nouveau défi * Crée un nouveau défi
*/ */
async createChallenge( async createChallenge(data: CreateChallengeInput): Promise<Challenge> {
data: CreateChallengeInput
): Promise<Challenge> {
// Vérifier que les deux joueurs existent // Vérifier que les deux joueurs existent
const [challenger, challenged] = await Promise.all([ const [challenger, challenged] = await Promise.all([
prisma.user.findUnique({ where: { id: data.challengerId } }), prisma.user.findUnique({ where: { id: data.challengerId } }),
@@ -103,9 +101,7 @@ export class ChallengeService {
// Vérifier que l'utilisateur est bien celui qui reçoit le défi // Vérifier que l'utilisateur est bien celui qui reçoit le défi
if (challenge.challengedId !== userId) { if (challenge.challengedId !== userId) {
throw new ValidationError( throw new ValidationError("Vous n'êtes pas autorisé à accepter ce défi");
"Vous n'êtes pas autorisé à accepter ce défi"
);
} }
// Vérifier que le défi est en attente // Vérifier que le défi est en attente
@@ -144,9 +140,7 @@ export class ChallengeService {
challenge.challengerId !== userId && challenge.challengerId !== userId &&
challenge.challengedId !== userId challenge.challengedId !== userId
) { ) {
throw new ValidationError( throw new ValidationError("Vous n'êtes pas autorisé à annuler ce défi");
"Vous n'êtes pas autorisé à annuler ce défi"
);
} }
// Vérifier que le défi peut être annulé // Vérifier que le défi peut être annulé
@@ -293,13 +287,17 @@ export class ChallengeService {
updateData.status = data.status; updateData.status = data.status;
} }
if (data.adminId !== undefined) { if (data.adminId !== undefined) {
updateData.admin = data.adminId ? { connect: { id: data.adminId } } : { disconnect: true }; updateData.admin = data.adminId
? { connect: { id: data.adminId } }
: { disconnect: true };
} }
if (data.adminComment !== undefined) { if (data.adminComment !== undefined) {
updateData.adminComment = data.adminComment; updateData.adminComment = data.adminComment;
} }
if (data.winnerId !== undefined) { if (data.winnerId !== undefined) {
updateData.winner = data.winnerId ? { connect: { id: data.winnerId } } : { disconnect: true }; updateData.winner = data.winnerId
? { connect: { id: data.winnerId } }
: { disconnect: true };
} }
return prisma.challenge.update({ return prisma.challenge.update({
@@ -370,10 +368,7 @@ export class ChallengeService {
async getUserChallenges(userId: string): Promise<ChallengeWithUsers[]> { async getUserChallenges(userId: string): Promise<ChallengeWithUsers[]> {
return prisma.challenge.findMany({ return prisma.challenge.findMany({
where: { where: {
OR: [ OR: [{ challengerId: userId }, { challengedId: userId }],
{ challengerId: userId },
{ challengedId: userId },
],
}, },
include: { include: {
challenger: { challenger: {
@@ -497,4 +492,3 @@ export class ChallengeService {
} }
export const challengeService = new ChallengeService(); export const challengeService = new ChallengeService();