Add dotenv package for environment variable management and update pnpm-lock.yaml. Adjust layout in RegisterPage and LoginPage components for improved responsiveness. Enhance AdminPanel with ChallengeManagement section and update navigation links for challenges. Refactor Prisma schema to include Challenge model and related enums.

This commit is contained in:
Julien Froidefond
2025-12-15 15:16:54 +01:00
parent f2bb02406e
commit bbb0fbb9a1
34 changed files with 11414 additions and 9081 deletions

View File

@@ -4,6 +4,7 @@ import { useState } from "react";
import UserManagement from "@/components/admin/UserManagement";
import EventManagement from "@/components/admin/EventManagement";
import FeedbackManagement from "@/components/admin/FeedbackManagement";
import ChallengeManagement from "@/components/admin/ChallengeManagement";
import BackgroundPreferences from "@/components/admin/BackgroundPreferences";
import { Button, Card, SectionTitle } from "@/components/ui";
@@ -18,7 +19,7 @@ interface AdminPanelProps {
initialPreferences: SitePreferences;
}
type AdminSection = "preferences" | "users" | "events" | "feedbacks";
type AdminSection = "preferences" | "users" | "events" | "feedbacks" | "challenges";
export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
const [activeSection, setActiveSection] =
@@ -67,6 +68,14 @@ export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
>
Feedbacks
</Button>
<Button
onClick={() => setActiveSection("challenges")}
variant={activeSection === "challenges" ? "primary" : "secondary"}
size="md"
className={activeSection === "challenges" ? "bg-pixel-gold/10" : ""}
>
Défis
</Button>
</div>
{activeSection === "preferences" && (
@@ -108,6 +117,15 @@ export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
<FeedbackManagement />
</Card>
)}
{activeSection === "challenges" && (
<Card variant="dark" className="p-6">
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
Gestion des Défis
</h2>
<ChallengeManagement />
</Card>
)}
</div>
</section>
);

View File

@@ -0,0 +1,504 @@
"use client";
import { useEffect, useState, useTransition } from "react";
import { validateChallenge, rejectChallenge, updateChallenge, deleteChallenge } from "@/actions/admin/challenges";
import { Button, Card, Input, Textarea, Alert } 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;
}
export default function ChallengeManagement() {
const [challenges, setChallenges] = useState<Challenge[]>([]);
const [loading, setLoading] = useState(true);
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);
useEffect(() => {
fetchChallenges();
}, []);
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);
} finally {
setLoading(false);
}
};
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();
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();
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();
setTimeout(() => setSuccessMessage(null), 5000);
} else {
setErrorMessage(result.error || "Erreur lors de la suppression");
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 en attente
</div>
);
}
const acceptedChallenges = challenges.filter((c) => c.status === "ACCEPTED");
const pendingChallenges = challenges.filter((c) => c.status === "PENDING");
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} défi{acceptedChallenges.length > 1 ? "s" : ""} en attente de validation
{pendingChallenges.length > 0 && (
<span className="ml-2">
{pendingChallenges.length} défi{pendingChallenges.length > 1 ? "s" : ""} en attente d'acceptation
</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-green-500/20 text-green-400"
: "bg-yellow-500/20 text-yellow-400"
}`}>
{challenge.status === "ACCEPTED" ? "Accepté" : "En attente d'acceptation"}
</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 === "ACCEPTED" && (
<Button
onClick={() => setSelectedChallenge(challenge)}
variant="primary"
size="sm"
>
Valider/Rejeter
</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 && (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
onClick={() => {
setSelectedChallenge(null);
setWinnerId("");
setAdminComment("");
}}
>
<Card
variant="dark"
className="max-w-2xl w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6">
<h2 className="text-2xl font-bold text-pixel-gold mb-4">
Valider/Rejeter le défi
</h2>
<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 ? "Validation..." : "Valider le défi"}
</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>
</Card>
</div>
)}
{/* Modal d'édition */}
{editingChallenge && (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
onClick={() => {
setEditingChallenge(null);
setEditTitle("");
setEditDescription("");
setEditPointsReward(0);
}}
>
<Card
variant="dark"
className="max-w-2xl w-full max-h-[90vh] overflow-y-auto"
onClick={(e) => e.stopPropagation()}
>
<div className="p-6">
<h2 className="text-2xl font-bold text-pixel-gold mb-4">
Modifier le défi
</h2>
<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>
</Card>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,428 @@
"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);
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 validation admin.");
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 "Accepté - En attente de validation admin";
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"
style={{
backgroundImage: `url(${backgroundImage})`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
>
<div className="absolute inset-0 bg-black/70 backdrop-blur-sm"></div>
<div className="relative z-10 w-full max-w-6xl mx-auto 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'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>
)}
</div>
</section>
);
}

View File

@@ -113,6 +113,21 @@ export default function Navigation({
>
LEADERBOARD
</Link>
{isAuthenticated && (
<Link
href="/challenges"
className="transition text-xs font-normal uppercase tracking-widest"
style={{ color: "var(--foreground)" }}
onMouseEnter={(e) =>
(e.currentTarget.style.color = "var(--accent-color)")
}
onMouseLeave={(e) =>
(e.currentTarget.style.color = "var(--foreground)")
}
>
DÉFIS
</Link>
)}
{isAdmin && (
<Link
href="/admin"
@@ -271,6 +286,22 @@ export default function Navigation({
>
LEADERBOARD
</Link>
{isAuthenticated && (
<Link
href="/challenges"
onClick={() => setIsMenuOpen(false)}
className="transition text-xs font-normal uppercase tracking-widest py-2"
style={{ color: "var(--foreground)" }}
onMouseEnter={(e) =>
(e.currentTarget.style.color = "var(--accent-color)")
}
onMouseLeave={(e) =>
(e.currentTarget.style.color = "var(--foreground)")
}
>
DÉFIS
</Link>
)}
{isAdmin && (
<Link
href="/admin"

View File

@@ -27,7 +27,8 @@ export default function Alert({
color: "var(--success)",
},
error: {
backgroundColor: "color-mix(in srgb, var(--destructive) 20%, transparent)",
backgroundColor:
"color-mix(in srgb, var(--destructive) 20%, transparent)",
borderColor: "color-mix(in srgb, var(--destructive) 50%, transparent)",
color: "var(--destructive)",
},
@@ -53,4 +54,3 @@ export default function Alert({
</div>
);
}