+
+ Extra Small
+
Small
diff --git a/components/challenges/ChallengeCard.tsx b/components/challenges/ChallengeCard.tsx
new file mode 100644
index 0000000..ff0c6d0
--- /dev/null
+++ b/components/challenges/ChallengeCard.tsx
@@ -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 (
+
+
+
+
+
+ {challenge.title}
+
+
+ {getStatusLabel(challenge.status)}
+
+
+
+
{challenge.description}
+
+
+
+
+
+ {challenge.challenger.username}
+
+
+
VS
+
+
+
+ {challenge.challenged.username}
+
+
+
+
+
+ Récompense:{" "}
+
+ {challenge.pointsReward} points
+
+
+
+ {challenge.winner && (
+
+ 🏆 Gagnant: {challenge.winner.username}
+
+ )}
+
+ {challenge.adminComment && (
+
+ Admin: {challenge.adminComment}
+
+ )}
+
+
+ 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")}`}
+
+
+
+
+ {canAccept && onAccept && (
+ onAccept(challenge.id)}
+ variant="primary"
+ size="sm"
+ disabled={isPending}
+ >
+ Accepter
+
+ )}
+ {canCancel && onCancel && (
+ {
+ if (confirm("Êtes-vous sûr de vouloir annuler ce défi ?")) {
+ onCancel(challenge.id);
+ }
+ }}
+ variant="secondary"
+ size="sm"
+ disabled={isPending}
+ >
+ Annuler
+
+ )}
+
+
+
+ );
+}
diff --git a/components/challenges/ChallengeForm.tsx b/components/challenges/ChallengeForm.tsx
new file mode 100644
index 0000000..b7b4feb
--- /dev/null
+++ b/components/challenges/ChallengeForm.tsx
@@ -0,0 +1,140 @@
+"use client";
+
+import { useState } from "react";
+import { Card, Input, Textarea, Button, Select } from "@/components/ui";
+
+interface User {
+ id: string;
+ username: string;
+ avatar: string | null;
+ score: number;
+ level: number;
+}
+
+interface ChallengeFormProps {
+ users: User[];
+ onSubmit: (data: {
+ challengedId: string;
+ title: string;
+ description: string;
+ pointsReward: number;
+ }) => void;
+ onCancel?: () => void;
+ isPending?: boolean;
+}
+
+export default function ChallengeForm({
+ users,
+ onSubmit,
+ onCancel,
+ isPending = false,
+}: ChallengeFormProps) {
+ const [challengedId, setChallengedId] = useState("");
+ const [title, setTitle] = useState("");
+ const [description, setDescription] = useState("");
+ const [pointsReward, setPointsReward] = useState(100);
+
+ const handleSubmit = (e: React.FormEvent) => {
+ e.preventDefault();
+ if (!challengedId || !title || !description) {
+ return;
+ }
+ onSubmit({
+ challengedId,
+ title,
+ description,
+ pointsReward,
+ });
+ };
+
+ const handleCancel = () => {
+ setChallengedId("");
+ setTitle("");
+ setDescription("");
+ setPointsReward(100);
+ onCancel?.();
+ };
+
+ return (
+
+
+ Créer un nouveau défi
+
+
+
+
+ );
+}
+
diff --git a/components/challenges/ChallengesSection.tsx b/components/challenges/ChallengesSection.tsx
index 1aa4a72..1811aa9 100644
--- a/components/challenges/ChallengesSection.tsx
+++ b/components/challenges/ChallengesSection.tsx
@@ -1,21 +1,15 @@
"use client";
-import { useEffect, useState, useTransition } from "react";
+import { 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";
+import { Button, Card, SectionTitle, Alert } from "@/components/ui";
+import ChallengeCard from "./ChallengeCard";
+import ChallengeForm from "./ChallengeForm";
interface User {
id: string;
@@ -52,33 +46,25 @@ interface Challenge {
}
interface ChallengesSectionProps {
+ initialChallenges: Challenge[];
+ initialUsers: User[];
backgroundImage: string;
}
export default function ChallengesSection({
+ initialChallenges,
+ initialUsers,
backgroundImage,
}: ChallengesSectionProps) {
const { data: session } = useSession();
- const [challenges, setChallenges] = useState
([]);
- const [users, setUsers] = useState([]);
- const [loading, setLoading] = useState(true);
+ const [challenges, setChallenges] = useState(initialChallenges);
+ const [users] = useState(initialUsers);
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(null);
const [errorMessage, setErrorMessage] = useState(null);
const [showExamples, setShowExamples] = useState(false);
- useEffect(() => {
- fetchChallenges();
- fetchUsers();
- }, []);
-
const fetchChallenges = async () => {
try {
const response = await fetch("/api/challenges");
@@ -88,45 +74,21 @@ export default function ChallengesSection({
}
} 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;
- }
-
+ const handleCreateChallenge = (data: {
+ challengedId: string;
+ title: string;
+ description: string;
+ pointsReward: number;
+ }) => {
startTransition(async () => {
- const result = await createChallenge({
- challengedId,
- title,
- description,
- pointsReward,
- });
+ const result = await createChallenge(data);
if (result.success) {
setSuccessMessage("Défi créé avec succès !");
setShowCreateForm(false);
- setChallengedId("");
- setTitle("");
- setDescription("");
- setPointsReward(100);
fetchChallenges();
setTimeout(() => setSuccessMessage(null), 5000);
} else {
@@ -141,7 +103,9 @@ export default function ChallengesSection({
const result = await acceptChallenge(challengeId);
if (result.success) {
- setSuccessMessage("Défi accepté ! En attente de désignation du gagnant.");
+ setSuccessMessage(
+ "Défi accepté ! En attente de désignation du gagnant."
+ );
fetchChallenges();
setTimeout(() => setSuccessMessage(null), 5000);
} else {
@@ -152,10 +116,6 @@ export default function ChallengesSection({
};
const handleCancelChallenge = (challengeId: string) => {
- if (!confirm("Êtes-vous sûr de vouloir annuler ce défi ?")) {
- return;
- }
-
startTransition(async () => {
const result = await cancelChallenge(challengeId);
@@ -170,40 +130,6 @@ export default function ChallengesSection({
});
};
- 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 (
{/* Background Image */}
@@ -254,84 +180,16 @@ export default function ChallengesSection({
{/* Create Form */}
{showCreateForm && (
-
-
- Créer un nouveau défi
-
-
-
-
-
- Défier qui ?
-
- setChallengedId(e.target.value)}
- className="w-full p-2 bg-black/60 border border-pixel-gold/30 rounded text-gray-300"
- >
- Sélectionner un joueur
- {users.map((user) => (
-
- {user.username} (Lv.{user.level} - {user.score} pts)
-
- ))}
-
-
-
-
-
- Titre du défi
-
- setTitle(e.target.value)}
- placeholder="Ex: Qui participera à plus d'événements ce mois ?"
- />
-
-
-
-
- Description
-
- setDescription(e.target.value)}
- placeholder="Décrivez les règles du défi..."
- rows={4}
- />
-
-
-
-
- Points à gagner (défaut: 100)
-
-
- setPointsReward(parseInt(e.target.value) || 100)
- }
- min={1}
- max={1000}
- />
-
-
-
- {isPending ? "Création..." : "Créer le défi"}
-
-
-
+ setShowCreateForm(false)}
+ isPending={isPending}
+ />
)}
{/* Challenges List */}
- {loading ? (
- Chargement...
- ) : challenges.length === 0 ? (
+ {challenges.length === 0 ? (
Vous n'avez aucun défi pour le moment.
@@ -339,118 +197,16 @@ export default function ChallengesSection({
) : (
- {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 (
-
-
-
-
-
- {challenge.title}
-
-
- {getStatusLabel(challenge.status)}
-
-
-
-
- {challenge.description}
-
-
-
-
-
-
- {challenge.challenger.username}
-
-
-
VS
-
-
-
- {challenge.challenged.username}
-
-
-
-
-
- Récompense:{" "}
-
- {challenge.pointsReward} points
-
-
-
- {challenge.winner && (
-
- 🏆 Gagnant: {challenge.winner.username}
-
- )}
-
- {challenge.adminComment && (
-
- Admin: {challenge.adminComment}
-
- )}
-
-
- 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")}`}
-
-
-
-
- {canAccept && (
- handleAcceptChallenge(challenge.id)}
- variant="primary"
- size="sm"
- disabled={isPending}
- >
- Accepter
-
- )}
- {canCancel && (
- handleCancelChallenge(challenge.id)}
- variant="secondary"
- size="sm"
- disabled={isPending}
- >
- Annuler
-
- )}
-
-
-
- );
- })}
+ {challenges.map((challenge) => (
+
+ ))}
)}
@@ -475,9 +231,9 @@ export default function ChallengesSection({
Qui participera à plus d'événements ce mois ?
- Le joueur qui participe au plus grand nombre d'événements
- organisés ce mois remporte le défi. Les événements doivent
- être validés par un admin pour compter.
+ Le joueur qui participe au plus grand nombre
+ d'événements organisés ce mois remporte le défi. Les
+ événements doivent être validés par un admin pour compter.
Points suggérés: 150
@@ -517,8 +273,8 @@ export default function ChallengesSection({
Le joueur qui accumule le plus de points cette semaine
- remporte le défi. Seuls les points gagnés après l'acceptation
- du défi comptent.
+ remporte le défi. Seuls les points gagnés après
+ l'acceptation du défi comptent.
Points suggérés: 250
@@ -530,9 +286,9 @@ export default function ChallengesSection({
Défi créatif : meilleure bio de profil
- Le joueur avec la bio de profil la plus créative et originale
- remporte le défi. L'admin désignera le gagnant selon
- l'originalité et la qualité de la bio.
+ Le joueur avec la bio de profil la plus créative et
+ originale remporte le défi. L'admin désignera le
+ gagnant selon l'originalité et la qualité de la bio.
Points suggérés: 120
diff --git a/components/ui/Badge.tsx b/components/ui/Badge.tsx
index 858de74..96cb066 100644
--- a/components/ui/Badge.tsx
+++ b/components/ui/Badge.tsx
@@ -5,7 +5,7 @@ import { HTMLAttributes, ReactNode } from "react";
interface BadgeProps extends HTMLAttributes
{
children: ReactNode;
variant?: "default" | "success" | "warning" | "danger" | "info";
- size?: "sm" | "md";
+ size?: "xs" | "sm" | "md";
}
const variantClasses = {
@@ -17,6 +17,7 @@ const variantClasses = {
};
const sizeClasses = {
+ xs: "px-1.5 py-0.5 text-[9px] sm:text-[10px]",
sm: "px-2 py-1 text-[10px] sm:text-xs",
md: "px-3 py-1 text-xs",
};
diff --git a/components/ui/Select.tsx b/components/ui/Select.tsx
new file mode 100644
index 0000000..c7e8604
--- /dev/null
+++ b/components/ui/Select.tsx
@@ -0,0 +1,39 @@
+"use client";
+
+import { SelectHTMLAttributes, forwardRef } from "react";
+
+interface SelectProps extends SelectHTMLAttributes {
+ label?: string;
+ error?: string;
+}
+
+const Select = forwardRef(
+ ({ label, error, className = "", children, ...props }, ref) => {
+ return (
+
+ {label && (
+
+ {label}
+
+ )}
+
+ {children}
+
+ {error && (
+
{error}
+ )}
+
+ );
+ }
+);
+
+Select.displayName = "Select";
+
+export default Select;
+
diff --git a/components/ui/index.ts b/components/ui/index.ts
index adc1fa8..a787cb5 100644
--- a/components/ui/index.ts
+++ b/components/ui/index.ts
@@ -2,6 +2,7 @@ export { default as Avatar } from "./Avatar";
export { default as Button } from "./Button";
export { default as Input } from "./Input";
export { default as Textarea } from "./Textarea";
+export { default as Select } from "./Select";
export { default as Card } from "./Card";
export { default as Modal } from "./Modal";
export { default as Badge } from "./Badge";