Files
got-gaming/components/challenges/ChallengesSection.tsx

549 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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);
const [showExamples, setShowExamples] = useState(false);
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&apos;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>
)}
{/* Examples Section */}
<Card variant="dark" className="mt-8">
<button
onClick={() => setShowExamples(!showExamples)}
className="w-full flex items-center justify-between p-4 text-left"
>
<h3 className="text-lg font-bold text-pixel-gold">
💡 Exemples de défis
</h3>
<span className="text-pixel-gold text-xl">
{showExamples ? "" : "+"}
</span>
</button>
{showExamples && (
<div className="px-4 pb-4 space-y-4 border-t border-[var(--border)] pt-4">
<div className="space-y-3">
<div className="p-4 bg-black/40 rounded border border-[var(--border)]">
<h4 className="font-bold text-pixel-gold mb-2">
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.
</p>
<div className="mt-2 text-xs text-gray-400">
Points suggérés: 150
</div>
</div>
<div className="p-4 bg-black/40 rounded border border-[var(--border)]">
<h4 className="font-bold text-pixel-gold mb-2">
Premier à atteindre le niveau 10
</h4>
<p className="text-sm text-gray-300">
Le premier joueur à atteindre le niveau 10 remporte le défi.
Le niveau est calculé automatiquement selon le score total.
</p>
<div className="mt-2 text-xs text-gray-400">
Points suggérés: 200
</div>
</div>
<div className="p-4 bg-black/40 rounded border border-[var(--border)]">
<h4 className="font-bold text-pixel-gold mb-2">
Meilleur feedback sur un événement
</h4>
<p className="text-sm text-gray-300">
Le joueur qui donne le feedback le plus détaillé et
constructif sur un événement remporte le défi. L&apos;admin
désignera le gagnant selon la qualité du feedback.
</p>
<div className="mt-2 text-xs text-gray-400">
Points suggérés: 100
</div>
</div>
<div className="p-4 bg-black/40 rounded border border-[var(--border)]">
<h4 className="font-bold text-pixel-gold mb-2">
Plus grand nombre de points gagnés cette semaine
</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.
</p>
<div className="mt-2 text-xs text-gray-400">
Points suggérés: 250
</div>
</div>
<div className="p-4 bg-black/40 rounded border border-[var(--border)]">
<h4 className="font-bold text-pixel-gold mb-2">
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.
</p>
<div className="mt-2 text-xs text-gray-400">
Points suggérés: 120
</div>
</div>
</div>
</div>
)}
</Card>
</div>
</section>
);
}