Files
got-gaming/components/admin/ChallengeManagement.tsx
2025-12-19 14:02:06 +01:00

704 lines
23 KiB
TypeScript

"use client";
import { useEffect, useState, useTransition } from "react";
import {
validateChallenge,
rejectChallenge,
updateChallenge,
deleteChallenge,
adminCancelChallenge,
reactivateChallenge,
adminAcceptChallenge,
} from "@/actions/admin/challenges";
import {
Button,
Card,
Input,
Textarea,
Alert,
Modal,
CloseButton,
} 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;
}
interface ChallengeManagementProps {
initialChallenges: Challenge[];
}
export default function ChallengeManagement({ initialChallenges }: ChallengeManagementProps) {
const [challenges, setChallenges] = useState<Challenge[]>(initialChallenges);
const [loading, setLoading] = useState(false);
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);
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);
}
};
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();
// Rafraîchir le badge des défis
window.dispatchEvent(new Event("refreshChallenges"));
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();
// Rafraîchir le badge des défis
window.dispatchEvent(new Event("refreshChallenges"));
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();
// Rafraîchir le badge des défis
window.dispatchEvent(new Event("refreshChallenges"));
setTimeout(() => setSuccessMessage(null), 5000);
} else {
setErrorMessage(result.error || "Erreur lors de la suppression");
setTimeout(() => setErrorMessage(null), 5000);
}
});
};
const handleCancel = async (challengeId: string) => {
if (!confirm("Êtes-vous sûr de vouloir annuler ce défi ?")) {
return;
}
startTransition(async () => {
const result = await adminCancelChallenge(challengeId);
if (result.success) {
setSuccessMessage("Défi annulé avec succès");
fetchChallenges();
// Rafraîchir le badge des défis
window.dispatchEvent(new Event("refreshChallenges"));
setTimeout(() => setSuccessMessage(null), 5000);
} else {
setErrorMessage(result.error || "Erreur lors de l'annulation");
setTimeout(() => setErrorMessage(null), 5000);
}
});
};
const handleReactivate = async (challengeId: string) => {
if (!confirm("Êtes-vous sûr de vouloir réactiver ce défi ?")) {
return;
}
startTransition(async () => {
const result = await reactivateChallenge(challengeId);
if (result.success) {
setSuccessMessage("Défi réactivé avec succès");
fetchChallenges();
// Rafraîchir le badge des défis
window.dispatchEvent(new Event("refreshChallenges"));
setTimeout(() => setSuccessMessage(null), 5000);
} else {
setErrorMessage(result.error || "Erreur lors de la réactivation");
setTimeout(() => setErrorMessage(null), 5000);
}
});
};
const handleAdminAccept = async (challengeId: string) => {
if (
!confirm(
"Êtes-vous sûr de vouloir accepter ce défi à la place de l'utilisateur ?"
)
) {
return;
}
startTransition(async () => {
const result = await adminAcceptChallenge(challengeId);
if (result.success) {
setSuccessMessage("Défi accepté avec succès");
fetchChallenges();
// Rafraîchir le badge des défis
window.dispatchEvent(new Event("refreshChallenges"));
setTimeout(() => setSuccessMessage(null), 5000);
} else {
setErrorMessage(result.error || "Erreur lors de l'acceptation");
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</div>;
}
const acceptedChallenges = challenges.filter((c) => c.status === "ACCEPTED");
const pendingChallenges = challenges.filter((c) => c.status === "PENDING");
const cancelledChallenges = challenges.filter(
(c) => c.status === "CANCELLED"
);
const completedChallenges = challenges.filter(
(c) => c.status === "COMPLETED"
);
const rejectedChallenges = challenges.filter((c) => c.status === "REJECTED");
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 > 0 && (
<span>
{acceptedChallenges.length} défi
{acceptedChallenges.length > 1 ? "s" : ""} en attente de désignation
du gagnant
</span>
)}
{pendingChallenges.length > 0 && (
<span className={acceptedChallenges.length > 0 ? "ml-2" : ""}>
{pendingChallenges.length} défi
{pendingChallenges.length > 1 ? "s" : ""} en attente
d&apos;acceptation
</span>
)}
{cancelledChallenges.length > 0 && (
<span className="ml-2">
{cancelledChallenges.length} défi
{cancelledChallenges.length > 1 ? "s" : ""} annulé
{cancelledChallenges.length > 1 ? "s" : ""}
</span>
)}
{completedChallenges.length > 0 && (
<span className="ml-2">
{completedChallenges.length} défi
{completedChallenges.length > 1 ? "s" : ""} complété
{completedChallenges.length > 1 ? "s" : ""}
</span>
)}
{rejectedChallenges.length > 0 && (
<span className="ml-2">
{rejectedChallenges.length} défi
{rejectedChallenges.length > 1 ? "s" : ""} rejeté
{rejectedChallenges.length > 1 ? "s" : ""}
</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-blue-500/20 text-blue-400"
: challenge.status === "COMPLETED"
? "bg-green-500/20 text-green-400"
: challenge.status === "CANCELLED"
? "bg-gray-500/20 text-gray-400"
: challenge.status === "REJECTED"
? "bg-red-500/20 text-red-400"
: "bg-yellow-500/20 text-yellow-400"
}`}
>
{challenge.status === "PENDING"
? "En attente d'acceptation"
: challenge.status === "ACCEPTED"
? "En cours - En attente de désignation du gagnant"
: challenge.status === "COMPLETED"
? "Complété"
: challenge.status === "CANCELLED"
? "Annulé"
: challenge.status === "REJECTED"
? "Rejeté"
: challenge.status}
</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 === "PENDING" && (
<Button
onClick={() => handleAdminAccept(challenge.id)}
variant="primary"
size="sm"
disabled={isPending}
>
Accepter le défi
</Button>
)}
{challenge.status === "ACCEPTED" && (
<Button
onClick={() => setSelectedChallenge(challenge)}
variant="primary"
size="sm"
>
Désigner le gagnant
</Button>
)}
{challenge.status !== "CANCELLED" &&
challenge.status !== "COMPLETED" && (
<Button
onClick={() => handleCancel(challenge.id)}
variant="secondary"
size="sm"
disabled={isPending}
>
Annuler
</Button>
)}
{challenge.status === "CANCELLED" && (
<Button
onClick={() => handleReactivate(challenge.id)}
variant="primary"
size="sm"
disabled={isPending}
>
Réactiver
</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 && (
<Modal
isOpen={!!selectedChallenge}
onClose={() => {
setSelectedChallenge(null);
setWinnerId("");
setAdminComment("");
}}
size="lg"
>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold text-pixel-gold">
Désigner le gagnant
</h2>
<CloseButton
onClick={() => {
setSelectedChallenge(null);
setWinnerId("");
setAdminComment("");
}}
size="lg"
/>
</div>
<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 ? "Enregistrement..." : "Confirmer le gagnant"}
</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>
</Modal>
)}
{/* Modal d'édition */}
{editingChallenge && (
<Modal
isOpen={!!editingChallenge}
onClose={() => {
setEditingChallenge(null);
setEditTitle("");
setEditDescription("");
setEditPointsReward(0);
}}
size="lg"
>
<div className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-bold text-pixel-gold">
Modifier le défi
</h2>
<CloseButton
onClick={() => {
setEditingChallenge(null);
setEditTitle("");
setEditDescription("");
setEditPointsReward(0);
}}
size="lg"
/>
</div>
<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>
</Modal>
)}
</div>
);
}