diff --git a/app/challenges/page.tsx b/app/challenges/page.tsx index 66cfda7..93db1c3 100644 --- a/app/challenges/page.tsx +++ b/app/challenges/page.tsx @@ -3,6 +3,8 @@ import { auth } from "@/lib/auth"; import { getBackgroundImage } from "@/lib/preferences"; import NavigationWrapper from "@/components/navigation/NavigationWrapper"; import ChallengesSection from "@/components/challenges/ChallengesSection"; +import { challengeService } from "@/services/challenges/challenge.service"; +import { userService } from "@/services/users/user.service"; export const dynamic = "force-dynamic"; @@ -13,15 +15,41 @@ export default async function ChallengesPage() { redirect("/login"); } - const backgroundImage = await getBackgroundImage( - "challenges", - "/got-2.jpg" - ); + const [challengesRaw, users, backgroundImage] = await Promise.all([ + challengeService.getUserChallenges(session.user.id), + userService + .getAllUsers({ + orderBy: { + username: "asc", + }, + select: { + id: true, + username: true, + avatar: true, + score: true, + level: true, + }, + }) + .then((users) => users.filter((user) => user.id !== session.user.id)), + getBackgroundImage("challenges", "/got-2.jpg"), + ]); + + // Convertir les dates Date en string pour correspondre au type attendu par le composant + const challenges = challengesRaw.map((challenge) => ({ + ...challenge, + createdAt: challenge.createdAt.toISOString(), + acceptedAt: challenge.acceptedAt?.toISOString() ?? null, + completedAt: challenge.completedAt?.toISOString() ?? null, + })); return (
- +
); } diff --git a/app/style-guide/page.tsx b/app/style-guide/page.tsx index 235b0b8..d8cbd4a 100644 --- a/app/style-guide/page.tsx +++ b/app/style-guide/page.tsx @@ -6,6 +6,7 @@ import { Button, Input, Textarea, + Select, Card, Badge, Alert, @@ -22,6 +23,7 @@ export default function StyleGuidePage() { const [modalOpen, setModalOpen] = useState(false); const [inputValue, setInputValue] = useState(""); const [textareaValue, setTextareaValue] = useState(""); + const [selectValue, setSelectValue] = useState(""); const [rating, setRating] = useState(0); return ( @@ -170,6 +172,74 @@ export default function StyleGuidePage() { + {/* Select */} + +

Select

+
+
+

Basique

+
+ +
+
+
+

Sans label

+
+ +
+
+
+

Avec erreur

+
+ +
+
+
+

Disabled

+
+ +
+
+
+
+ {/* Badges */}

Badges

@@ -187,6 +257,9 @@ export default function StyleGuidePage() {

Tailles

+ + 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 && ( + + )} + {canCancel && onCancel && ( + + )} +
+
+
+ ); +} 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 +

+ +
+ + +
+ + setTitle(e.target.value)} + placeholder="Ex: Qui participera à plus d'événements ce mois ?" + /> +
+ +
+ +