Refactor ChallengesSection component to utilize initial challenges and users data: Replace fetching logic with props for challenges and users, streamline challenge creation with a dedicated form component, and enhance UI for better user experience.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m49s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m49s
This commit is contained in:
183
components/challenges/ChallengeCard.tsx
Normal file
183
components/challenges/ChallengeCard.tsx
Normal file
@@ -0,0 +1,183 @@
|
||||
"use client";
|
||||
|
||||
import { Card, Button, Avatar, Badge } from "@/components/ui";
|
||||
|
||||
interface ChallengeCardProps {
|
||||
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;
|
||||
};
|
||||
currentUserId?: string;
|
||||
onAccept?: (challengeId: string) => void;
|
||||
onCancel?: (challengeId: string) => void;
|
||||
isPending?: boolean;
|
||||
}
|
||||
|
||||
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 getStatusVariant = (
|
||||
status: string
|
||||
): "default" | "success" | "warning" | "danger" | "info" => {
|
||||
switch (status) {
|
||||
case "PENDING":
|
||||
return "warning";
|
||||
case "ACCEPTED":
|
||||
return "info";
|
||||
case "COMPLETED":
|
||||
return "success";
|
||||
case "REJECTED":
|
||||
return "danger";
|
||||
case "CANCELLED":
|
||||
return "default";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
export default function ChallengeCard({
|
||||
challenge,
|
||||
currentUserId,
|
||||
onAccept,
|
||||
onCancel,
|
||||
isPending = false,
|
||||
}: ChallengeCardProps) {
|
||||
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 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 flex-wrap">
|
||||
<h3 className="text-lg font-bold text-pixel-gold">
|
||||
{challenge.title}
|
||||
</h3>
|
||||
<Badge variant={getStatusVariant(challenge.status)} size="xs">
|
||||
{getStatusLabel(challenge.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-300 mb-4">{challenge.description}</p>
|
||||
|
||||
<div className="flex items-center gap-4 mb-2 flex-wrap">
|
||||
<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 && onAccept && (
|
||||
<Button
|
||||
onClick={() => onAccept(challenge.id)}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
disabled={isPending}
|
||||
>
|
||||
Accepter
|
||||
</Button>
|
||||
)}
|
||||
{canCancel && onCancel && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (confirm("Êtes-vous sûr de vouloir annuler ce défi ?")) {
|
||||
onCancel(challenge.id);
|
||||
}
|
||||
}}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={isPending}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user