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:
428
components/challenges/ChallengesSection.tsx
Normal file
428
components/challenges/ChallengesSection.tsx
Normal file
@@ -0,0 +1,428 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, 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";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
score: number;
|
||||
level: number;
|
||||
}
|
||||
|
||||
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;
|
||||
winner?: {
|
||||
id: string;
|
||||
username: string;
|
||||
} | null;
|
||||
createdAt: string;
|
||||
acceptedAt: string | null;
|
||||
completedAt: string | null;
|
||||
}
|
||||
|
||||
interface ChallengesSectionProps {
|
||||
backgroundImage: string;
|
||||
}
|
||||
|
||||
export default function ChallengesSection({ backgroundImage }: ChallengesSectionProps) {
|
||||
const { data: session } = useSession();
|
||||
const [challenges, setChallenges] = useState<Challenge[]>([]);
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
fetchChallenges();
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const fetchChallenges = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/challenges");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setChallenges(data);
|
||||
}
|
||||
} 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;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await createChallenge({
|
||||
challengedId,
|
||||
title,
|
||||
description,
|
||||
pointsReward,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
setSuccessMessage("Défi créé avec succès !");
|
||||
setShowCreateForm(false);
|
||||
setChallengedId("");
|
||||
setTitle("");
|
||||
setDescription("");
|
||||
setPointsReward(100);
|
||||
fetchChallenges();
|
||||
setTimeout(() => setSuccessMessage(null), 5000);
|
||||
} else {
|
||||
setErrorMessage(result.error || "Erreur lors de la création du défi");
|
||||
setTimeout(() => setErrorMessage(null), 5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleAcceptChallenge = (challengeId: string) => {
|
||||
startTransition(async () => {
|
||||
const result = await acceptChallenge(challengeId);
|
||||
|
||||
if (result.success) {
|
||||
setSuccessMessage("Défi accepté ! En attente de validation admin.");
|
||||
fetchChallenges();
|
||||
setTimeout(() => setSuccessMessage(null), 5000);
|
||||
} else {
|
||||
setErrorMessage(result.error || "Erreur lors de l'acceptation");
|
||||
setTimeout(() => setErrorMessage(null), 5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancelChallenge = (challengeId: string) => {
|
||||
if (!confirm("Êtes-vous sûr de vouloir annuler ce défi ?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await cancelChallenge(challengeId);
|
||||
|
||||
if (result.success) {
|
||||
setSuccessMessage("Défi annulé");
|
||||
fetchChallenges();
|
||||
setTimeout(() => setSuccessMessage(null), 5000);
|
||||
} else {
|
||||
setErrorMessage(result.error || "Erreur lors de l'annulation");
|
||||
setTimeout(() => setErrorMessage(null), 5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "En attente d'acceptation";
|
||||
case "ACCEPTED":
|
||||
return "Accepté - En attente de validation admin";
|
||||
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"
|
||||
style={{
|
||||
backgroundImage: `url(${backgroundImage})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm"></div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
|
||||
<SectionTitle variant="gradient" size="md" className="mb-8 text-center">
|
||||
DÉFIS ENTRE JOUEURS
|
||||
</SectionTitle>
|
||||
|
||||
{successMessage && (
|
||||
<Alert variant="success" className="mb-4">
|
||||
{successMessage}
|
||||
</Alert>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<Alert variant="error" className="mb-4">
|
||||
{errorMessage}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<div className="mb-6 flex justify-center">
|
||||
<Button
|
||||
onClick={() => setShowCreateForm(!showCreateForm)}
|
||||
variant="primary"
|
||||
size="md"
|
||||
>
|
||||
{showCreateForm ? "Annuler" : "Créer un défi"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
{/* Challenges List */}
|
||||
{loading ? (
|
||||
<div className="text-center text-pixel-gold py-8">Chargement...</div>
|
||||
) : challenges.length === 0 ? (
|
||||
<Card variant="dark" className="p-6 text-center">
|
||||
<p className="text-gray-400">
|
||||
Vous n'avez aucun défi pour le moment.
|
||||
</p>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user