Refactor ChallengesSection component to utilize initial challenges and users data: Replace fetching logic with props for challenges and users, streamline challenge creation with a dedicated form component, and enhance UI for better user experience.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m49s

This commit is contained in:
Julien Froidefond
2025-12-16 08:20:40 +01:00
parent c7595c4173
commit a9a4120874
8 changed files with 516 additions and 295 deletions

View File

@@ -1,21 +1,15 @@
"use client";
import { useEffect, useState, useTransition } from "react";
import { useState, useTransition } from "react";
import { useSession } from "next-auth/react";
import {
createChallenge,
acceptChallenge,
cancelChallenge,
} from "@/actions/challenges/create";
import {
Button,
Card,
SectionTitle,
Input,
Textarea,
Alert,
} from "@/components/ui";
import { Avatar } from "@/components/ui";
import { Button, Card, SectionTitle, Alert } from "@/components/ui";
import ChallengeCard from "./ChallengeCard";
import ChallengeForm from "./ChallengeForm";
interface User {
id: string;
@@ -52,33 +46,25 @@ interface Challenge {
}
interface ChallengesSectionProps {
initialChallenges: Challenge[];
initialUsers: User[];
backgroundImage: string;
}
export default function ChallengesSection({
initialChallenges,
initialUsers,
backgroundImage,
}: ChallengesSectionProps) {
const { data: session } = useSession();
const [challenges, setChallenges] = useState<Challenge[]>([]);
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
const [challenges, setChallenges] = useState<Challenge[]>(initialChallenges);
const [users] = useState<User[]>(initialUsers);
const [showCreateForm, setShowCreateForm] = useState(false);
const [isPending, startTransition] = useTransition();
// Form state
const [challengedId, setChallengedId] = useState("");
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [pointsReward, setPointsReward] = useState(100);
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [showExamples, setShowExamples] = useState(false);
useEffect(() => {
fetchChallenges();
fetchUsers();
}, []);
const fetchChallenges = async () => {
try {
const response = await fetch("/api/challenges");
@@ -88,45 +74,21 @@ export default function ChallengesSection({
}
} catch (error) {
console.error("Error fetching challenges:", error);
} finally {
setLoading(false);
}
};
const fetchUsers = async () => {
try {
const response = await fetch("/api/users");
if (response.ok) {
const data = await response.json();
setUsers(data);
}
} catch (error) {
console.error("Error fetching users:", error);
}
};
const handleCreateChallenge = () => {
if (!challengedId || !title || !description) {
setErrorMessage("Veuillez remplir tous les champs");
setTimeout(() => setErrorMessage(null), 5000);
return;
}
const handleCreateChallenge = (data: {
challengedId: string;
title: string;
description: string;
pointsReward: number;
}) => {
startTransition(async () => {
const result = await createChallenge({
challengedId,
title,
description,
pointsReward,
});
const result = await createChallenge(data);
if (result.success) {
setSuccessMessage("Défi créé avec succès !");
setShowCreateForm(false);
setChallengedId("");
setTitle("");
setDescription("");
setPointsReward(100);
fetchChallenges();
setTimeout(() => setSuccessMessage(null), 5000);
} else {
@@ -141,7 +103,9 @@ export default function ChallengesSection({
const result = await acceptChallenge(challengeId);
if (result.success) {
setSuccessMessage("Défi accepté ! En attente de désignation du gagnant.");
setSuccessMessage(
"Défi accepté ! En attente de désignation du gagnant."
);
fetchChallenges();
setTimeout(() => setSuccessMessage(null), 5000);
} else {
@@ -152,10 +116,6 @@ export default function ChallengesSection({
};
const handleCancelChallenge = (challengeId: string) => {
if (!confirm("Êtes-vous sûr de vouloir annuler ce défi ?")) {
return;
}
startTransition(async () => {
const result = await cancelChallenge(challengeId);
@@ -170,40 +130,6 @@ export default function ChallengesSection({
});
};
const getStatusLabel = (status: string) => {
switch (status) {
case "PENDING":
return "En attente d'acceptation";
case "ACCEPTED":
return "En cours - En attente de désignation du gagnant";
case "COMPLETED":
return "Complété";
case "REJECTED":
return "Rejeté";
case "CANCELLED":
return "Annulé";
default:
return status;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case "PENDING":
return "text-yellow-400";
case "ACCEPTED":
return "text-blue-400";
case "COMPLETED":
return "text-green-400";
case "REJECTED":
return "text-red-400";
case "CANCELLED":
return "text-gray-400";
default:
return "text-gray-300";
}
};
return (
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16">
{/* Background Image */}
@@ -254,84 +180,16 @@ export default function ChallengesSection({
{/* Create Form */}
{showCreateForm && (
<Card variant="dark" className="p-6 mb-8">
<h2 className="text-xl font-bold text-pixel-gold mb-4">
Créer un nouveau défi
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-bold text-pixel-gold mb-2">
Défier qui ?
</label>
<select
value={challengedId}
onChange={(e) => setChallengedId(e.target.value)}
className="w-full p-2 bg-black/60 border border-pixel-gold/30 rounded text-gray-300"
>
<option value="">Sélectionner un joueur</option>
{users.map((user) => (
<option key={user.id} value={user.id}>
{user.username} (Lv.{user.level} - {user.score} pts)
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-bold text-pixel-gold mb-2">
Titre du défi
</label>
<Input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Ex: Qui participera à plus d'événements ce mois ?"
/>
</div>
<div>
<label className="block text-sm font-bold text-pixel-gold mb-2">
Description
</label>
<Textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Décrivez les règles du défi..."
rows={4}
/>
</div>
<div>
<label className="block text-sm font-bold text-pixel-gold mb-2">
Points à gagner (défaut: 100)
</label>
<Input
type="number"
value={pointsReward}
onChange={(e) =>
setPointsReward(parseInt(e.target.value) || 100)
}
min={1}
max={1000}
/>
</div>
<Button
onClick={handleCreateChallenge}
variant="primary"
disabled={isPending || !challengedId || !title || !description}
className="w-full"
>
{isPending ? "Création..." : "Créer le défi"}
</Button>
</div>
</Card>
<ChallengeForm
users={users}
onSubmit={handleCreateChallenge}
onCancel={() => setShowCreateForm(false)}
isPending={isPending}
/>
)}
{/* Challenges List */}
{loading ? (
<div className="text-center text-pixel-gold py-8">Chargement...</div>
) : challenges.length === 0 ? (
{challenges.length === 0 ? (
<Card variant="dark" className="p-6 text-center">
<p className="text-gray-400">
Vous n&apos;avez aucun défi pour le moment.
@@ -339,118 +197,16 @@ export default function ChallengesSection({
</Card>
) : (
<div className="space-y-4">
{challenges.map((challenge) => {
const currentUserId = session?.user?.id;
const isChallenger = challenge.challenger.id === currentUserId;
const isChallenged = challenge.challenged.id === currentUserId;
const canAccept = challenge.status === "PENDING" && isChallenged;
const canCancel =
(challenge.status === "PENDING" ||
challenge.status === "ACCEPTED") &&
(isChallenger || isChallenged);
return (
<Card key={challenge.id} variant="dark" className="p-6">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<h3 className="text-lg font-bold text-pixel-gold">
{challenge.title}
</h3>
<span
className={`text-xs px-2 py-1 rounded ${getStatusColor(
challenge.status
)} bg-black/40`}
>
{getStatusLabel(challenge.status)}
</span>
</div>
<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-2">
<Avatar
src={challenge.challenger.avatar}
username={challenge.challenger.username}
size="sm"
/>
<span className="text-sm text-gray-300">
{challenge.challenger.username}
</span>
</div>
<span className="text-gray-500">VS</span>
<div className="flex items-center gap-2">
<Avatar
src={challenge.challenged.avatar}
username={challenge.challenged.username}
size="sm"
/>
<span className="text-sm text-gray-300">
{challenge.challenged.username}
</span>
</div>
</div>
<div className="text-sm text-gray-400">
Récompense:{" "}
<span className="text-pixel-gold font-bold">
{challenge.pointsReward} points
</span>
</div>
{challenge.winner && (
<div className="text-sm text-green-400 mt-2">
🏆 Gagnant: {challenge.winner.username}
</div>
)}
{challenge.adminComment && (
<div className="text-xs text-gray-500 mt-2 italic">
Admin: {challenge.adminComment}
</div>
)}
<div className="text-xs text-gray-500 mt-2">
Créé le:{" "}
{new Date(challenge.createdAt).toLocaleDateString(
"fr-FR"
)}
{challenge.acceptedAt &&
` • Accepté le: ${new Date(challenge.acceptedAt).toLocaleDateString("fr-FR")}`}
{challenge.completedAt &&
` • Complété le: ${new Date(challenge.completedAt).toLocaleDateString("fr-FR")}`}
</div>
</div>
<div className="flex flex-col gap-2">
{canAccept && (
<Button
onClick={() => handleAcceptChallenge(challenge.id)}
variant="primary"
size="sm"
disabled={isPending}
>
Accepter
</Button>
)}
{canCancel && (
<Button
onClick={() => handleCancelChallenge(challenge.id)}
variant="secondary"
size="sm"
disabled={isPending}
>
Annuler
</Button>
)}
</div>
</div>
</Card>
);
})}
{challenges.map((challenge) => (
<ChallengeCard
key={challenge.id}
challenge={challenge}
currentUserId={session?.user?.id}
onAccept={handleAcceptChallenge}
onCancel={handleCancelChallenge}
isPending={isPending}
/>
))}
</div>
)}
@@ -475,9 +231,9 @@ export default function ChallengesSection({
Qui participera à plus d&apos;événements ce mois ?
</h4>
<p className="text-sm text-gray-300">
Le joueur qui participe au plus grand nombre d&apos;événements
organisés ce mois remporte le défi. Les événements doivent
être validés par un admin pour compter.
Le joueur qui participe au plus grand nombre
d&apos;événements organisés ce mois remporte le défi. Les
événements doivent être validés par un admin pour compter.
</p>
<div className="mt-2 text-xs text-gray-400">
Points suggérés: 150
@@ -517,8 +273,8 @@ export default function ChallengesSection({
</h4>
<p className="text-sm text-gray-300">
Le joueur qui accumule le plus de points cette semaine
remporte le défi. Seuls les points gagnés après l&apos;acceptation
du défi comptent.
remporte le défi. Seuls les points gagnés après
l&apos;acceptation du défi comptent.
</p>
<div className="mt-2 text-xs text-gray-400">
Points suggérés: 250
@@ -530,9 +286,9 @@ export default function ChallengesSection({
Défi créatif : meilleure bio de profil
</h4>
<p className="text-sm text-gray-300">
Le joueur avec la bio de profil la plus créative et originale
remporte le défi. L&apos;admin désignera le gagnant selon
l&apos;originalité et la qualité de la bio.
Le joueur avec la bio de profil la plus créative et
originale remporte le défi. L&apos;admin désignera le
gagnant selon l&apos;originalité et la qualité de la bio.
</p>
<div className="mt-2 text-xs text-gray-400">
Points suggérés: 120