Files
got-gaming/components/admin/ChallengeManagement.tsx

537 lines
17 KiB
TypeScript

"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<Challenge[]>([]);
const [loading, setLoading] = useState(true);
const [selectedChallenge, setSelectedChallenge] = useState<Challenge | null>(
null
);
const [editingChallenge, setEditingChallenge] = useState<Challenge | null>(
null
);
const [winnerId, setWinnerId] = useState<string>("");
const [adminComment, setAdminComment] = useState("");
const [editTitle, setEditTitle] = useState("");
const [editDescription, setEditDescription] = useState("");
const [editPointsReward, setEditPointsReward] = useState<number>(0);
const [isPending, startTransition] = useTransition();
const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(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 (
<div className="text-center text-pixel-gold py-8">Chargement...</div>
);
}
if (challenges.length === 0) {
return (
<div className="text-center text-gray-400 py-8">
Aucun défi en attente
</div>
);
}
const acceptedChallenges = challenges.filter((c) => c.status === "ACCEPTED");
const pendingChallenges = challenges.filter((c) => c.status === "PENDING");
return (
<div className="space-y-4">
{successMessage && (
<Alert variant="success" className="mb-4">
{successMessage}
</Alert>
)}
{errorMessage && (
<Alert variant="error" className="mb-4">
{errorMessage}
</Alert>
)}
<div className="text-sm text-gray-400 mb-4">
{acceptedChallenges.length} défi
{acceptedChallenges.length > 1 ? "s" : ""} en attente de validation
{pendingChallenges.length > 0 && (
<span className="ml-2">
{pendingChallenges.length} défi
{pendingChallenges.length > 1 ? "s" : ""} en attente
d&apos;acceptation
</span>
)}
</div>
{challenges.map((challenge) => (
<Card key={challenge.id} variant="dark" className="p-4">
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h3 className="text-lg font-bold text-pixel-gold mb-2">
{challenge.title}
</h3>
<p className="text-gray-300 mb-4">{challenge.description}</p>
<div className="flex items-center gap-4 mb-4">
<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>
<span className="text-xs text-gray-500">VS</span>
<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>
<div className="text-xs mt-2">
<span
className={`px-2 py-1 rounded ${
challenge.status === "ACCEPTED"
? "bg-green-500/20 text-green-400"
: "bg-yellow-500/20 text-yellow-400"
}`}
>
{challenge.status === "ACCEPTED"
? "Accepté"
: "En attente d'acceptation"}
</span>
</div>
{challenge.acceptedAt && (
<div className="text-xs text-gray-500 mt-2">
Accepté le:{" "}
{new Date(challenge.acceptedAt).toLocaleDateString("fr-FR")}
</div>
)}
</div>
<div className="flex flex-col gap-2">
<Button
onClick={() => handleEdit(challenge)}
variant="secondary"
size="sm"
>
Modifier
</Button>
{challenge.status === "ACCEPTED" && (
<Button
onClick={() => setSelectedChallenge(challenge)}
variant="primary"
size="sm"
>
Valider/Rejeter
</Button>
)}
<Button
onClick={() => handleDelete(challenge.id)}
variant="secondary"
size="sm"
className="text-destructive hover:text-destructive"
style={{
color: "var(--destructive)",
borderColor: "var(--destructive)",
}}
>
Supprimer
</Button>
</div>
</div>
</Card>
))}
{/* Modal de validation */}
{selectedChallenge && (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
onClick={() => {
setSelectedChallenge(null);
setWinnerId("");
setAdminComment("");
}}
>
<Card
variant="dark"
className="max-w-2xl w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6">
<h2 className="text-2xl font-bold text-pixel-gold mb-4">
Valider/Rejeter le défi
</h2>
<div className="mb-6">
<h3 className="text-lg font-bold text-gray-300 mb-2">
{selectedChallenge.title}
</h3>
<p className="text-gray-400 mb-4">
{selectedChallenge.description}
</p>
<div className="flex items-center gap-4 mb-4">
<div className="flex items-center gap-2">
<Avatar
src={selectedChallenge.challenger.avatar}
username={selectedChallenge.challenger.username}
size="md"
/>
<span className="text-gray-300">
{selectedChallenge.challenger.username}
</span>
</div>
<span className="text-gray-500">VS</span>
<div className="flex items-center gap-2">
<Avatar
src={selectedChallenge.challenged.avatar}
username={selectedChallenge.challenged.username}
size="md"
/>
<span className="text-gray-300">
{selectedChallenge.challenged.username}
</span>
</div>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-bold text-pixel-gold mb-2">
Sélectionner le gagnant
</label>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="winner"
value={selectedChallenge.challenger.id}
checked={winnerId === selectedChallenge.challenger.id}
onChange={(e) => setWinnerId(e.target.value)}
className="w-4 h-4"
/>
<span className="text-gray-300">
{selectedChallenge.challenger.username}
</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="winner"
value={selectedChallenge.challenged.id}
checked={winnerId === selectedChallenge.challenged.id}
onChange={(e) => setWinnerId(e.target.value)}
className="w-4 h-4"
/>
<span className="text-gray-300">
{selectedChallenge.challenged.username}
</span>
</label>
</div>
</div>
<div className="mb-4">
<label className="block text-sm font-bold text-pixel-gold mb-2">
Commentaire (optionnel)
</label>
<textarea
value={adminComment}
onChange={(e) => setAdminComment(e.target.value)}
className="w-full p-2 bg-black/60 border border-pixel-gold/30 rounded text-gray-300"
rows={3}
placeholder="Commentaire pour les joueurs..."
/>
</div>
<div className="flex gap-4">
<Button
onClick={handleValidate}
variant="primary"
disabled={!winnerId || isPending}
className="flex-1"
>
{isPending ? "Validation..." : "Valider le défi"}
</Button>
<Button
onClick={handleReject}
variant="secondary"
disabled={isPending}
className="flex-1"
>
{isPending ? "Rejet..." : "Rejeter le défi"}
</Button>
<Button
onClick={() => {
setSelectedChallenge(null);
setWinnerId("");
setAdminComment("");
}}
variant="secondary"
disabled={isPending}
>
Annuler
</Button>
</div>
</div>
</Card>
</div>
)}
{/* Modal d'édition */}
{editingChallenge && (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
onClick={() => {
setEditingChallenge(null);
setEditTitle("");
setEditDescription("");
setEditPointsReward(0);
}}
>
<Card
variant="dark"
className="max-w-2xl w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6">
<h2 className="text-2xl font-bold text-pixel-gold mb-4">
Modifier le défi
</h2>
<div className="space-y-4">
<Input
id="edit-title"
label="Titre"
value={editTitle}
onChange={(e) => setEditTitle(e.target.value)}
required
placeholder="Titre du défi"
/>
<Textarea
id="edit-description"
label="Description"
value={editDescription}
onChange={(e) => setEditDescription(e.target.value)}
required
rows={4}
placeholder="Description du défi"
/>
<Input
id="edit-points"
label="Récompense (points)"
type="number"
min="1"
value={editPointsReward}
onChange={(e) =>
setEditPointsReward(parseInt(e.target.value) || 0)
}
required
placeholder="100"
/>
<div className="flex gap-4 pt-4">
<Button
onClick={handleUpdate}
variant="primary"
disabled={
isPending ||
!editTitle ||
!editDescription ||
editPointsReward <= 0
}
className="flex-1"
>
{isPending ? "Mise à jour..." : "Enregistrer"}
</Button>
<Button
onClick={() => {
setEditingChallenge(null);
setEditTitle("");
setEditDescription("");
setEditPointsReward(0);
}}
variant="secondary"
disabled={isPending}
>
Annuler
</Button>
</div>
</div>
</div>
</Card>
</div>
)}
</div>
);
}