diff --git a/actions/admin/challenges.ts b/actions/admin/challenges.ts new file mode 100644 index 0000000..de5073e --- /dev/null +++ b/actions/admin/challenges.ts @@ -0,0 +1,152 @@ +'use server' + +import { revalidatePath } from 'next/cache' +import { auth } from '@/lib/auth' +import { challengeService } from '@/services/challenges/challenge.service' +import { Role } from '@/prisma/generated/prisma/client' +import { + ValidationError, + NotFoundError, +} from '@/services/errors' + +async function checkAdminAccess() { + const session = await auth() + if (!session?.user || session.user.role !== Role.ADMIN) { + throw new Error('Accès refusé - Admin uniquement') + } + return session +} + +export async function validateChallenge( + challengeId: string, + winnerId: string, + adminComment?: string +) { + try { + const session = await checkAdminAccess() + + const challenge = await challengeService.validateChallenge( + challengeId, + session.user.id, + winnerId, + adminComment + ) + + revalidatePath('/admin') + revalidatePath('/challenges') + revalidatePath('/leaderboard') + + return { success: true, message: 'Défi validé avec succès', data: challenge } + } catch (error) { + console.error('Validate challenge error:', error) + + if (error instanceof ValidationError) { + return { success: false, error: error.message } + } + if (error instanceof NotFoundError) { + return { success: false, error: error.message } + } + if (error instanceof Error && error.message.includes('Accès refusé')) { + return { success: false, error: error.message } + } + + return { success: false, error: 'Une erreur est survenue lors de la validation du défi' } + } +} + +export async function rejectChallenge( + challengeId: string, + adminComment?: string +) { + try { + const session = await checkAdminAccess() + + const challenge = await challengeService.rejectChallenge( + challengeId, + session.user.id, + adminComment + ) + + revalidatePath('/admin') + revalidatePath('/challenges') + + return { success: true, message: 'Défi rejeté', data: challenge } + } catch (error) { + console.error('Reject challenge error:', error) + + if (error instanceof ValidationError) { + return { success: false, error: error.message } + } + if (error instanceof NotFoundError) { + return { success: false, error: error.message } + } + if (error instanceof Error && error.message.includes('Accès refusé')) { + return { success: false, error: error.message } + } + + return { success: false, error: 'Une erreur est survenue lors du rejet du défi' } + } +} + +export async function updateChallenge( + challengeId: string, + data: { + title?: string + description?: string + pointsReward?: number + } +) { + try { + const session = await checkAdminAccess() + + const challenge = await challengeService.updateChallenge(challengeId, { + title: data.title, + description: data.description, + pointsReward: data.pointsReward, + }) + + revalidatePath('/admin') + revalidatePath('/challenges') + + return { success: true, message: 'Défi mis à jour avec succès', data: challenge } + } catch (error) { + console.error('Update challenge error:', error) + + if (error instanceof ValidationError) { + return { success: false, error: error.message } + } + if (error instanceof NotFoundError) { + return { success: false, error: error.message } + } + if (error instanceof Error && error.message.includes('Accès refusé')) { + return { success: false, error: error.message } + } + + return { success: false, error: 'Une erreur est survenue lors de la mise à jour du défi' } + } +} + +export async function deleteChallenge(challengeId: string) { + try { + const session = await checkAdminAccess() + + await challengeService.deleteChallenge(challengeId) + + revalidatePath('/admin') + revalidatePath('/challenges') + + return { success: true, message: 'Défi supprimé avec succès' } + } catch (error) { + console.error('Delete challenge error:', error) + + if (error instanceof NotFoundError) { + return { success: false, error: error.message } + } + if (error instanceof Error && error.message.includes('Accès refusé')) { + return { success: false, error: error.message } + } + + return { success: false, error: 'Une erreur est survenue lors de la suppression du défi' } + } +} + diff --git a/actions/challenges/create.ts b/actions/challenges/create.ts new file mode 100644 index 0000000..fc2b67a --- /dev/null +++ b/actions/challenges/create.ts @@ -0,0 +1,112 @@ +'use server' + +import { revalidatePath } from 'next/cache' +import { auth } from '@/lib/auth' +import { challengeService } from '@/services/challenges/challenge.service' +import { + ValidationError, + NotFoundError, + ConflictError, +} from '@/services/errors' + +export async function createChallenge(data: { + challengedId: string + title: string + description: string + pointsReward?: number +}) { + try { + const session = await auth() + + if (!session?.user?.id) { + return { success: false, error: 'Vous devez être connecté pour créer un défi' } + } + + const challenge = await challengeService.createChallenge({ + challengerId: session.user.id, + challengedId: data.challengedId, + title: data.title, + description: data.description, + pointsReward: data.pointsReward || 100, + }) + + revalidatePath('/challenges') + revalidatePath('/profile') + + return { success: true, message: 'Défi créé avec succès', data: challenge } + } catch (error) { + console.error('Create challenge error:', error) + + if (error instanceof ValidationError || error instanceof ConflictError) { + return { success: false, error: error.message } + } + if (error instanceof NotFoundError) { + return { success: false, error: error.message } + } + + return { success: false, error: 'Une erreur est survenue lors de la création du défi' } + } +} + +export async function acceptChallenge(challengeId: string) { + try { + const session = await auth() + + if (!session?.user?.id) { + return { success: false, error: 'Vous devez être connecté pour accepter un défi' } + } + + const challenge = await challengeService.acceptChallenge( + challengeId, + session.user.id + ) + + revalidatePath('/challenges') + revalidatePath('/profile') + + return { success: true, message: 'Défi accepté', data: challenge } + } catch (error) { + console.error('Accept challenge error:', error) + + if (error instanceof ValidationError) { + return { success: false, error: error.message } + } + if (error instanceof NotFoundError) { + return { success: false, error: error.message } + } + + return { success: false, error: 'Une erreur est survenue lors de l\'acceptation du défi' } + } +} + +export async function cancelChallenge(challengeId: string) { + try { + const session = await auth() + + if (!session?.user?.id) { + return { success: false, error: 'Vous devez être connecté pour annuler un défi' } + } + + const challenge = await challengeService.cancelChallenge( + challengeId, + session.user.id + ) + + revalidatePath('/challenges') + revalidatePath('/profile') + + return { success: true, message: 'Défi annulé', data: challenge } + } catch (error) { + console.error('Cancel challenge error:', error) + + if (error instanceof ValidationError) { + return { success: false, error: error.message } + } + if (error instanceof NotFoundError) { + return { success: false, error: error.message } + } + + return { success: false, error: 'Une erreur est survenue lors de l\'annulation du défi' } + } +} + diff --git a/app/api/admin/challenges/route.ts b/app/api/admin/challenges/route.ts new file mode 100644 index 0000000..e447672 --- /dev/null +++ b/app/api/admin/challenges/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { challengeService } from "@/services/challenges/challenge.service"; +import { Role } from "@/prisma/generated/prisma/client"; + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user || session.user.role !== Role.ADMIN) { + return NextResponse.json({ error: "Accès refusé" }, { status: 403 }); + } + + // Récupérer tous les défis (PENDING et ACCEPTED) pour l'admin + const allChallenges = await challengeService.getAllChallenges(); + // Filtrer pour ne garder que PENDING et ACCEPTED + const challenges = allChallenges.filter( + (c) => c.status === "PENDING" || c.status === "ACCEPTED" + ); + + return NextResponse.json(challenges); + } catch (error) { + console.error("Error fetching challenges:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération des défis" }, + { status: 500 } + ); + } +} + diff --git a/app/api/challenges/route.ts b/app/api/challenges/route.ts new file mode 100644 index 0000000..4db2d1c --- /dev/null +++ b/app/api/challenges/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { challengeService } from "@/services/challenges/challenge.service"; + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Vous devez être connecté" }, { status: 401 }); + } + + // Récupérer tous les défis de l'utilisateur + const challenges = await challengeService.getUserChallenges(session.user.id); + + return NextResponse.json(challenges); + } catch (error) { + console.error("Error fetching challenges:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération des défis" }, + { status: 500 } + ); + } +} + diff --git a/app/api/users/route.ts b/app/api/users/route.ts new file mode 100644 index 0000000..e3a924a --- /dev/null +++ b/app/api/users/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from "next/server"; +import { auth } from "@/lib/auth"; +import { userService } from "@/services/users/user.service"; + +export async function GET() { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Vous devez être connecté" }, { status: 401 }); + } + + // Récupérer tous les utilisateurs (pour sélectionner qui défier) + const users = await userService.getAllUsers({ + orderBy: { + username: "asc", + }, + select: { + id: true, + username: true, + avatar: true, + score: true, + level: true, + }, + }); + + // Filtrer l'utilisateur actuel + const otherUsers = users.filter((user) => user.id !== session.user.id); + + return NextResponse.json(otherUsers); + } catch (error) { + console.error("Error fetching users:", error); + return NextResponse.json( + { error: "Erreur lors de la récupération des utilisateurs" }, + { status: 500 } + ); + } +} + diff --git a/app/challenges/page.tsx b/app/challenges/page.tsx new file mode 100644 index 0000000..944a981 --- /dev/null +++ b/app/challenges/page.tsx @@ -0,0 +1,28 @@ +import { redirect } from "next/navigation"; +import { auth } from "@/lib/auth"; +import { getBackgroundImage } from "@/lib/preferences"; +import NavigationWrapper from "@/components/navigation/NavigationWrapper"; +import ChallengesSection from "@/components/challenges/ChallengesSection"; + +export const dynamic = "force-dynamic"; + +export default async function ChallengesPage() { + const session = await auth(); + + if (!session?.user) { + redirect("/login"); + } + + const backgroundImage = await getBackgroundImage( + "home", + "/got-background.jpg" + ); + + return ( +
+ + +
+ ); +} + diff --git a/app/login/page.tsx b/app/login/page.tsx index fc31174..69b6604 100644 --- a/app/login/page.tsx +++ b/app/login/page.tsx @@ -60,7 +60,7 @@ export default function LoginPage() { CONNEXION diff --git a/app/register/page.tsx b/app/register/page.tsx index 23b580e..a1471a7 100644 --- a/app/register/page.tsx +++ b/app/register/page.tsx @@ -174,7 +174,7 @@ export default function RegisterPage() { {/* Register Form */} -
+
Classe de Personnage (optionnel) -
+
{CHARACTER_CLASSES.map((cls) => ( ))}
diff --git a/components/admin/AdminPanel.tsx b/components/admin/AdminPanel.tsx index d10df95..e22033d 100644 --- a/components/admin/AdminPanel.tsx +++ b/components/admin/AdminPanel.tsx @@ -4,6 +4,7 @@ import { useState } from "react"; import UserManagement from "@/components/admin/UserManagement"; import EventManagement from "@/components/admin/EventManagement"; import FeedbackManagement from "@/components/admin/FeedbackManagement"; +import ChallengeManagement from "@/components/admin/ChallengeManagement"; import BackgroundPreferences from "@/components/admin/BackgroundPreferences"; import { Button, Card, SectionTitle } from "@/components/ui"; @@ -18,7 +19,7 @@ interface AdminPanelProps { initialPreferences: SitePreferences; } -type AdminSection = "preferences" | "users" | "events" | "feedbacks"; +type AdminSection = "preferences" | "users" | "events" | "feedbacks" | "challenges"; export default function AdminPanel({ initialPreferences }: AdminPanelProps) { const [activeSection, setActiveSection] = @@ -67,6 +68,14 @@ export default function AdminPanel({ initialPreferences }: AdminPanelProps) { > Feedbacks +
{activeSection === "preferences" && ( @@ -108,6 +117,15 @@ export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
)} + + {activeSection === "challenges" && ( + +

+ Gestion des Défis +

+ +
+ )}
); diff --git a/components/admin/ChallengeManagement.tsx b/components/admin/ChallengeManagement.tsx new file mode 100644 index 0000000..f19e07b --- /dev/null +++ b/components/admin/ChallengeManagement.tsx @@ -0,0 +1,504 @@ +"use client"; + +import { useEffect, useState, useTransition } from "react"; +import { validateChallenge, rejectChallenge, updateChallenge, deleteChallenge } from "@/actions/admin/challenges"; +import { Button, Card, Input, Textarea, Alert } from "@/components/ui"; +import { Avatar } from "@/components/ui"; + +interface Challenge { + id: string; + challenger: { + id: string; + username: string; + avatar: string | null; + }; + challenged: { + id: string; + username: string; + avatar: string | null; + }; + title: string; + description: string; + pointsReward: number; + status: string; + adminComment: string | null; + createdAt: string; + acceptedAt: string | null; +} + +export default function ChallengeManagement() { + const [challenges, setChallenges] = useState([]); + const [loading, setLoading] = useState(true); + const [selectedChallenge, setSelectedChallenge] = useState(null); + const [editingChallenge, setEditingChallenge] = useState(null); + const [winnerId, setWinnerId] = useState(""); + const [adminComment, setAdminComment] = useState(""); + const [editTitle, setEditTitle] = useState(""); + const [editDescription, setEditDescription] = useState(""); + const [editPointsReward, setEditPointsReward] = useState(0); + const [isPending, startTransition] = useTransition(); + const [successMessage, setSuccessMessage] = useState(null); + const [errorMessage, setErrorMessage] = useState(null); + + useEffect(() => { + fetchChallenges(); + }, []); + + const fetchChallenges = async () => { + try { + const response = await fetch("/api/admin/challenges"); + if (response.ok) { + const data = await response.json(); + setChallenges(data); + } + } catch (error) { + console.error("Error fetching challenges:", error); + } finally { + setLoading(false); + } + }; + + const handleValidate = async () => { + if (!selectedChallenge || !winnerId) { + setErrorMessage("Veuillez sélectionner un gagnant"); + setTimeout(() => setErrorMessage(null), 5000); + return; + } + + startTransition(async () => { + const result = await validateChallenge( + selectedChallenge.id, + winnerId, + adminComment || undefined + ); + + if (result.success) { + setSuccessMessage("Défi validé avec succès ! Les points ont été attribués."); + setSelectedChallenge(null); + setWinnerId(""); + setAdminComment(""); + fetchChallenges(); + setTimeout(() => setSuccessMessage(null), 5000); + } else { + setErrorMessage(result.error || "Erreur lors de la validation"); + setTimeout(() => setErrorMessage(null), 5000); + } + }); + }; + + const handleReject = async () => { + if (!selectedChallenge) return; + + if (!confirm("Êtes-vous sûr de vouloir rejeter ce défi ?")) { + return; + } + + startTransition(async () => { + const result = await rejectChallenge( + selectedChallenge.id, + adminComment || undefined + ); + + if (result.success) { + setSuccessMessage("Défi rejeté"); + setSelectedChallenge(null); + setAdminComment(""); + fetchChallenges(); + setTimeout(() => setSuccessMessage(null), 5000); + } else { + setErrorMessage(result.error || "Erreur lors du rejet"); + setTimeout(() => setErrorMessage(null), 5000); + } + }); + }; + + const handleEdit = (challenge: Challenge) => { + setEditingChallenge(challenge); + setEditTitle(challenge.title); + setEditDescription(challenge.description); + setEditPointsReward(challenge.pointsReward); + }; + + const handleUpdate = async () => { + if (!editingChallenge) return; + + startTransition(async () => { + const result = await updateChallenge(editingChallenge.id, { + title: editTitle, + description: editDescription, + pointsReward: editPointsReward, + }); + + if (result.success) { + setSuccessMessage("Défi mis à jour avec succès"); + setEditingChallenge(null); + setEditTitle(""); + setEditDescription(""); + setEditPointsReward(0); + fetchChallenges(); + setTimeout(() => setSuccessMessage(null), 5000); + } else { + setErrorMessage(result.error || "Erreur lors de la mise à jour"); + setTimeout(() => setErrorMessage(null), 5000); + } + }); + }; + + const handleDelete = async (challengeId: string) => { + if (!confirm("Êtes-vous sûr de vouloir supprimer ce défi ? Cette action est irréversible.")) { + return; + } + + startTransition(async () => { + const result = await deleteChallenge(challengeId); + + if (result.success) { + setSuccessMessage("Défi supprimé avec succès"); + fetchChallenges(); + setTimeout(() => setSuccessMessage(null), 5000); + } else { + setErrorMessage(result.error || "Erreur lors de la suppression"); + setTimeout(() => setErrorMessage(null), 5000); + } + }); + }; + + if (loading) { + return ( +
Chargement...
+ ); + } + + if (challenges.length === 0) { + return ( +
+ Aucun défi en attente +
+ ); + } + + const acceptedChallenges = challenges.filter((c) => c.status === "ACCEPTED"); + const pendingChallenges = challenges.filter((c) => c.status === "PENDING"); + + return ( +
+ {successMessage && ( + + {successMessage} + + )} + {errorMessage && ( + + {errorMessage} + + )} +
+ {acceptedChallenges.length} défi{acceptedChallenges.length > 1 ? "s" : ""} en attente de validation + {pendingChallenges.length > 0 && ( + + • {pendingChallenges.length} défi{pendingChallenges.length > 1 ? "s" : ""} en attente d'acceptation + + )} +
+ + {challenges.map((challenge) => ( + +
+
+

+ {challenge.title} +

+

{challenge.description}

+ +
+
+ + + {challenge.challenger.username} + + VS + + + {challenge.challenged.username} + +
+
+ +
+ Récompense: {challenge.pointsReward} points +
+
+ + {challenge.status === "ACCEPTED" ? "Accepté" : "En attente d'acceptation"} + +
+ {challenge.acceptedAt && ( +
+ Accepté le: {new Date(challenge.acceptedAt).toLocaleDateString("fr-FR")} +
+ )} +
+ +
+ + {challenge.status === "ACCEPTED" && ( + + )} + +
+
+
+ ))} + + {/* Modal de validation */} + {selectedChallenge && ( +
{ + setSelectedChallenge(null); + setWinnerId(""); + setAdminComment(""); + }} + > + e.stopPropagation()} + > +
+

+ Valider/Rejeter le défi +

+ +
+

+ {selectedChallenge.title} +

+

+ {selectedChallenge.description} +

+ +
+
+ + + {selectedChallenge.challenger.username} + +
+ VS +
+ + + {selectedChallenge.challenged.username} + +
+
+
+ +
+ +
+ + +
+
+ +
+ +