Refactor modal implementation across admin components: Replace Card components with a reusable Modal component in ChallengeManagement, EventManagement, and UserManagement, enhancing UI consistency and maintainability. Update Modal to use React portals for improved rendering.
This commit is contained in:
@@ -9,7 +9,7 @@ import {
|
|||||||
adminCancelChallenge,
|
adminCancelChallenge,
|
||||||
reactivateChallenge,
|
reactivateChallenge,
|
||||||
} from "@/actions/admin/challenges";
|
} from "@/actions/admin/challenges";
|
||||||
import { Button, Card, Input, Textarea, Alert } from "@/components/ui";
|
import { Button, Card, Input, Textarea, Alert, Modal, CloseButton } from "@/components/ui";
|
||||||
import { Avatar } from "@/components/ui";
|
import { Avatar } from "@/components/ui";
|
||||||
|
|
||||||
interface Challenge {
|
interface Challenge {
|
||||||
@@ -417,23 +417,29 @@ export default function ChallengeManagement() {
|
|||||||
|
|
||||||
{/* Modal de validation */}
|
{/* Modal de validation */}
|
||||||
{selectedChallenge && (
|
{selectedChallenge && (
|
||||||
<div
|
<Modal
|
||||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
isOpen={!!selectedChallenge}
|
||||||
|
onClose={() => {
|
||||||
|
setSelectedChallenge(null);
|
||||||
|
setWinnerId("");
|
||||||
|
setAdminComment("");
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-2xl font-bold text-pixel-gold">
|
||||||
|
Désigner le gagnant
|
||||||
|
</h2>
|
||||||
|
<CloseButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedChallenge(null);
|
setSelectedChallenge(null);
|
||||||
setWinnerId("");
|
setWinnerId("");
|
||||||
setAdminComment("");
|
setAdminComment("");
|
||||||
}}
|
}}
|
||||||
>
|
size="lg"
|
||||||
<Card
|
/>
|
||||||
variant="dark"
|
</div>
|
||||||
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">
|
|
||||||
Désigner le gagnant
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h3 className="text-lg font-bold text-gray-300 mb-2">
|
<h3 className="text-lg font-bold text-gray-300 mb-2">
|
||||||
@@ -545,30 +551,36 @@ export default function ChallengeManagement() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Modal>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modal d'édition */}
|
{/* Modal d'édition */}
|
||||||
{editingChallenge && (
|
{editingChallenge && (
|
||||||
<div
|
<Modal
|
||||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
isOpen={!!editingChallenge}
|
||||||
|
onClose={() => {
|
||||||
|
setEditingChallenge(null);
|
||||||
|
setEditTitle("");
|
||||||
|
setEditDescription("");
|
||||||
|
setEditPointsReward(0);
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-2xl font-bold text-pixel-gold">
|
||||||
|
Modifier le défi
|
||||||
|
</h2>
|
||||||
|
<CloseButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingChallenge(null);
|
setEditingChallenge(null);
|
||||||
setEditTitle("");
|
setEditTitle("");
|
||||||
setEditDescription("");
|
setEditDescription("");
|
||||||
setEditPointsReward(0);
|
setEditPointsReward(0);
|
||||||
}}
|
}}
|
||||||
>
|
size="lg"
|
||||||
<Card
|
/>
|
||||||
variant="dark"
|
</div>
|
||||||
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">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
@@ -632,8 +644,7 @@ export default function ChallengeManagement() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Modal>
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,15 @@
|
|||||||
import { useState, useEffect, useTransition } from "react";
|
import { useState, useEffect, useTransition } from "react";
|
||||||
import { calculateEventStatus } from "@/lib/eventStatus";
|
import { calculateEventStatus } from "@/lib/eventStatus";
|
||||||
import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events";
|
import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events";
|
||||||
import { Input, Textarea, Button, Card, Badge } from "@/components/ui";
|
import {
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Badge,
|
||||||
|
Modal,
|
||||||
|
CloseButton,
|
||||||
|
} from "@/components/ui";
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -221,11 +229,20 @@ export default function EventManagement() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Modal de création/édition */}
|
||||||
{(isCreating || editingEvent) && (
|
{(isCreating || editingEvent) && (
|
||||||
<Card variant="default" className="p-3 sm:p-4 mb-4">
|
<Modal
|
||||||
<h4 className="text-pixel-gold font-bold mb-4 text-base sm:text-lg break-words">
|
isOpen={isCreating || !!editingEvent}
|
||||||
|
onClose={handleCancel}
|
||||||
|
size="lg"
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||||
{isCreating ? "Créer un événement" : "Modifier l'événement"}
|
{isCreating ? "Créer un événement" : "Modifier l'événement"}
|
||||||
</h4>
|
</h4>
|
||||||
|
<CloseButton onClick={handleCancel} size="lg" />
|
||||||
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
type="date"
|
type="date"
|
||||||
@@ -330,7 +347,8 @@ export default function EventManagement() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</div>
|
||||||
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{events.length === 0 ? (
|
{events.length === 0 ? (
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, useTransition } from "react";
|
import { useState, useEffect, useTransition } from "react";
|
||||||
import { Avatar, Input, Button, Card } from "@/components/ui";
|
import {
|
||||||
|
Avatar,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Modal,
|
||||||
|
CloseButton,
|
||||||
|
} from "@/components/ui";
|
||||||
import { updateUser, deleteUser } from "@/actions/admin/users";
|
import { updateUser, deleteUser } from "@/actions/admin/users";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@@ -159,6 +166,25 @@ export default function UserManagement() {
|
|||||||
return num.toLocaleString("en-US");
|
return num.toLocaleString("en-US");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Trouver l'utilisateur en cours d'édition pour les previews
|
||||||
|
const currentEditingUserData = editingUser
|
||||||
|
? users.find((u) => u.id === editingUser.userId)
|
||||||
|
: null;
|
||||||
|
const previewHp =
|
||||||
|
currentEditingUserData && editingUser
|
||||||
|
? Math.max(
|
||||||
|
0,
|
||||||
|
Math.min(
|
||||||
|
currentEditingUserData.maxHp,
|
||||||
|
currentEditingUserData.hp + editingUser.hpDelta
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
const previewXp =
|
||||||
|
currentEditingUserData && editingUser
|
||||||
|
? Math.max(0, currentEditingUserData.xp + editingUser.xpDelta)
|
||||||
|
: 0;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
|
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
|
||||||
}
|
}
|
||||||
@@ -171,26 +197,14 @@ export default function UserManagement() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
users.map((user) => {
|
users.map((user) => {
|
||||||
const isEditing = editingUser?.userId === user.id;
|
|
||||||
const previewHp = isEditing
|
|
||||||
? Math.max(0, Math.min(user.maxHp, user.hp + editingUser.hpDelta))
|
|
||||||
: user.hp;
|
|
||||||
const previewXp = isEditing
|
|
||||||
? Math.max(0, user.xp + editingUser.xpDelta)
|
|
||||||
: user.xp;
|
|
||||||
const displayAvatar = isEditing ? editingUser.avatar : user.avatar;
|
|
||||||
const displayUsername = isEditing
|
|
||||||
? editingUser.username || user.username
|
|
||||||
: user.username;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={user.id} variant="default" className="p-3 sm:p-4">
|
<Card key={user.id} variant="default" className="p-3 sm:p-4">
|
||||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-2">
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-2">
|
||||||
<div className="flex gap-2 sm:gap-3 items-center flex-1 min-w-0">
|
<div className="flex gap-2 sm:gap-3 items-center flex-1 min-w-0">
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
<Avatar
|
<Avatar
|
||||||
src={displayAvatar}
|
src={user.avatar}
|
||||||
username={displayUsername}
|
username={user.username}
|
||||||
size="sm"
|
size="sm"
|
||||||
className="flex-shrink-0"
|
className="flex-shrink-0"
|
||||||
borderClassName="border-2 border-pixel-gold/50"
|
borderClassName="border-2 border-pixel-gold/50"
|
||||||
@@ -198,7 +212,7 @@ export default function UserManagement() {
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-1.5 sm:gap-2 flex-wrap">
|
<div className="flex items-center gap-1.5 sm:gap-2 flex-wrap">
|
||||||
<h3 className="text-pixel-gold font-bold text-sm sm:text-base break-words">
|
<h3 className="text-pixel-gold font-bold text-sm sm:text-base break-words">
|
||||||
{displayUsername}
|
{user.username}
|
||||||
</h3>
|
</h3>
|
||||||
<span className="text-[10px] sm:text-xs text-gray-500 whitespace-nowrap">
|
<span className="text-[10px] sm:text-xs text-gray-500 whitespace-nowrap">
|
||||||
Niveau {user.level}
|
Niveau {user.level}
|
||||||
@@ -221,7 +235,6 @@ export default function UserManagement() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!isEditing && (
|
|
||||||
<div className="flex gap-2 flex-shrink-0 sm:ml-2">
|
<div className="flex gap-2 flex-shrink-0 sm:ml-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleEdit(user)}
|
onClick={() => handleEdit(user)}
|
||||||
@@ -239,10 +252,64 @@ export default function UserManagement() {
|
|||||||
: "Supprimer"}
|
: "Supprimer"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isEditing ? (
|
{/* Affichage des stats */}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4 text-[10px] sm:text-xs">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex justify-between items-center mb-0.5">
|
||||||
|
<span className="text-gray-400">HP</span>
|
||||||
|
<span className="text-gray-400">
|
||||||
|
{user.hp}/{user.maxHp}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 bg-black/60 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-gradient-to-r from-red-600 to-green-500"
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(
|
||||||
|
100,
|
||||||
|
(user.hp / user.maxHp) * 100
|
||||||
|
)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex justify-between items-center mb-0.5">
|
||||||
|
<span className="text-gray-400">XP</span>
|
||||||
|
<span className="text-gray-400">
|
||||||
|
{formatNumber(user.xp)}/{formatNumber(user.maxXp)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="h-1.5 bg-black/60 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-full bg-gradient-to-r from-blue-600 to-purple-500"
|
||||||
|
style={{
|
||||||
|
width: `${Math.min(
|
||||||
|
100,
|
||||||
|
(user.xp / user.maxXp) * 100
|
||||||
|
)}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal d'édition */}
|
||||||
|
{editingUser && currentEditingUserData && (
|
||||||
|
<Modal isOpen={!!editingUser} onClose={handleCancel} size="lg">
|
||||||
|
<div className="p-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||||
|
Modifier l'utilisateur
|
||||||
|
</h4>
|
||||||
|
<CloseButton onClick={handleCancel} size="lg" />
|
||||||
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{/* Username Section */}
|
{/* Username Section */}
|
||||||
<Input
|
<Input
|
||||||
@@ -269,15 +336,15 @@ export default function UserManagement() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={editingUser.avatar}
|
src={editingUser.avatar}
|
||||||
username={editingUser.username || user.username}
|
username={
|
||||||
|
editingUser.username || currentEditingUserData.username
|
||||||
|
}
|
||||||
size="lg"
|
size="lg"
|
||||||
borderClassName="border-2 border-pixel-gold/50"
|
borderClassName="border-2 border-pixel-gold/50"
|
||||||
/>
|
/>
|
||||||
{uploadingAvatar === user.id && (
|
{uploadingAvatar === editingUser.userId && (
|
||||||
<div className="absolute inset-0 bg-black/60 flex items-center justify-center rounded-full">
|
<div className="absolute inset-0 bg-black/60 flex items-center justify-center rounded-full">
|
||||||
<div className="text-pixel-gold text-xs">
|
<div className="text-pixel-gold text-xs">Upload...</div>
|
||||||
Upload...
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -330,7 +397,7 @@ export default function UserManagement() {
|
|||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
|
|
||||||
setUploadingAvatar(user.id);
|
setUploadingAvatar(editingUser.userId);
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
@@ -363,16 +430,16 @@ export default function UserManagement() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
id={`avatar-upload-${user.id}`}
|
id={`avatar-upload-${editingUser.userId}`}
|
||||||
/>
|
/>
|
||||||
<label htmlFor={`avatar-upload-${user.id}`}>
|
<label htmlFor={`avatar-upload-${editingUser.userId}`}>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
as="span"
|
as="span"
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
>
|
>
|
||||||
{uploadingAvatar === user.id
|
{uploadingAvatar === editingUser.userId
|
||||||
? "Upload en cours..."
|
? "Upload en cours..."
|
||||||
: "Upload un avatar custom"}
|
: "Upload un avatar custom"}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -387,7 +454,7 @@ export default function UserManagement() {
|
|||||||
Points de Vie (HP)
|
Points de Vie (HP)
|
||||||
</label>
|
</label>
|
||||||
<span className="text-[10px] sm:text-xs text-gray-400">
|
<span className="text-[10px] sm:text-xs text-gray-400">
|
||||||
{previewHp} / {user.maxHp}
|
{previewHp} / {currentEditingUserData.maxHp}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 sm:gap-2 flex-wrap">
|
<div className="flex gap-1 sm:gap-2 flex-wrap">
|
||||||
@@ -453,7 +520,7 @@ export default function UserManagement() {
|
|||||||
style={{
|
style={{
|
||||||
width: `${Math.min(
|
width: `${Math.min(
|
||||||
100,
|
100,
|
||||||
(previewHp / user.maxHp) * 100
|
(previewHp / currentEditingUserData.maxHp) * 100
|
||||||
)}%`,
|
)}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -467,7 +534,8 @@ export default function UserManagement() {
|
|||||||
Expérience (XP)
|
Expérience (XP)
|
||||||
</label>
|
</label>
|
||||||
<span className="text-[10px] sm:text-xs text-gray-400">
|
<span className="text-[10px] sm:text-xs text-gray-400">
|
||||||
{formatNumber(previewXp)} / {formatNumber(user.maxXp)}
|
{formatNumber(previewXp)} /{" "}
|
||||||
|
{formatNumber(currentEditingUserData.maxXp)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1 sm:gap-2 flex-wrap">
|
<div className="flex gap-1 sm:gap-2 flex-wrap">
|
||||||
@@ -533,7 +601,7 @@ export default function UserManagement() {
|
|||||||
style={{
|
style={{
|
||||||
width: `${Math.min(
|
width: `${Math.min(
|
||||||
100,
|
100,
|
||||||
(previewXp / user.maxXp) * 100
|
(previewXp / currentEditingUserData.maxXp) * 100
|
||||||
)}%`,
|
)}%`,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -695,60 +763,13 @@ export default function UserManagement() {
|
|||||||
>
|
>
|
||||||
{saving ? "Enregistrement..." : "Enregistrer"}
|
{saving ? "Enregistrement..." : "Enregistrer"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button onClick={handleCancel} variant="secondary" size="md">
|
||||||
onClick={handleCancel}
|
|
||||||
variant="secondary"
|
|
||||||
size="md"
|
|
||||||
>
|
|
||||||
Annuler
|
Annuler
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4 text-[10px] sm:text-xs">
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex justify-between items-center mb-0.5">
|
|
||||||
<span className="text-gray-400">HP</span>
|
|
||||||
<span className="text-gray-400">
|
|
||||||
{user.hp}/{user.maxHp}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1.5 bg-black/60 rounded-full overflow-hidden">
|
</Modal>
|
||||||
<div
|
|
||||||
className="h-full bg-gradient-to-r from-red-600 to-green-500"
|
|
||||||
style={{
|
|
||||||
width: `${Math.min(
|
|
||||||
100,
|
|
||||||
(user.hp / user.maxHp) * 100
|
|
||||||
)}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<div className="flex justify-between items-center mb-0.5">
|
|
||||||
<span className="text-gray-400">XP</span>
|
|
||||||
<span className="text-gray-400">
|
|
||||||
{formatNumber(user.xp)}/{formatNumber(user.maxXp)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="h-1.5 bg-black/60 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full bg-gradient-to-r from-blue-600 to-purple-500"
|
|
||||||
style={{
|
|
||||||
width: `${Math.min(
|
|
||||||
100,
|
|
||||||
(user.xp / user.maxXp) * 100
|
|
||||||
)}%`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ReactNode, useEffect } from "react";
|
import { ReactNode, useEffect } from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -37,7 +38,7 @@ export default function Modal({
|
|||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
const modalContent = (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 backdrop-blur-sm"
|
className="fixed inset-0 z-[200] flex items-center justify-center p-4 backdrop-blur-sm"
|
||||||
style={{
|
style={{
|
||||||
@@ -59,4 +60,11 @@ export default function Modal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Utiliser un portal pour rendre le modal directement dans le body
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
return createPortal(modalContent, document.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user