All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 6m16s
459 lines
15 KiB
TypeScript
459 lines
15 KiB
TypeScript
"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 désignation du gagnant.");
|
|
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 "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 */}
|
|
<div
|
|
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
|
style={{
|
|
backgroundImage: `url('${backgroundImage}')`,
|
|
}}
|
|
>
|
|
{/* Dark overlay for readability */}
|
|
<div
|
|
className="absolute inset-0 bg-gradient-to-b"
|
|
style={{
|
|
background: `linear-gradient(to bottom,
|
|
color-mix(in srgb, var(--background) 70%, transparent),
|
|
color-mix(in srgb, var(--background) 60%, transparent),
|
|
color-mix(in srgb, var(--background) 80%, transparent)
|
|
)`,
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className="relative z-10 w-full max-w-6xl mx-auto px-4 sm: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>
|
|
);
|
|
}
|