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}
|
||||||
onClick={() => {
|
onClose={() => {
|
||||||
setSelectedChallenge(null);
|
setSelectedChallenge(null);
|
||||||
setWinnerId("");
|
setWinnerId("");
|
||||||
setAdminComment("");
|
setAdminComment("");
|
||||||
}}
|
}}
|
||||||
|
size="lg"
|
||||||
>
|
>
|
||||||
<Card
|
<div className="p-6">
|
||||||
variant="dark"
|
<div className="flex items-center justify-between mb-4">
|
||||||
className="max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
<h2 className="text-2xl font-bold text-pixel-gold">
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="p-6">
|
|
||||||
<h2 className="text-2xl font-bold text-pixel-gold mb-4">
|
|
||||||
Désigner le gagnant
|
Désigner le gagnant
|
||||||
</h2>
|
</h2>
|
||||||
|
<CloseButton
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedChallenge(null);
|
||||||
|
setWinnerId("");
|
||||||
|
setAdminComment("");
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<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">
|
||||||
@@ -544,31 +550,37 @@ export default function ChallengeManagement() {
|
|||||||
Annuler
|
Annuler
|
||||||
</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}
|
||||||
onClick={() => {
|
onClose={() => {
|
||||||
setEditingChallenge(null);
|
setEditingChallenge(null);
|
||||||
setEditTitle("");
|
setEditTitle("");
|
||||||
setEditDescription("");
|
setEditDescription("");
|
||||||
setEditPointsReward(0);
|
setEditPointsReward(0);
|
||||||
}}
|
}}
|
||||||
|
size="lg"
|
||||||
>
|
>
|
||||||
<Card
|
<div className="p-6">
|
||||||
variant="dark"
|
<div className="flex items-center justify-between mb-4">
|
||||||
className="max-w-2xl w-full max-h-[90vh] overflow-y-auto"
|
<h2 className="text-2xl font-bold text-pixel-gold">
|
||||||
onClick={(e) => e.stopPropagation()}
|
|
||||||
>
|
|
||||||
<div className="p-6">
|
|
||||||
<h2 className="text-2xl font-bold text-pixel-gold mb-4">
|
|
||||||
Modifier le défi
|
Modifier le défi
|
||||||
</h2>
|
</h2>
|
||||||
|
<CloseButton
|
||||||
|
onClick={() => {
|
||||||
|
setEditingChallenge(null);
|
||||||
|
setEditTitle("");
|
||||||
|
setEditDescription("");
|
||||||
|
setEditPointsReward(0);
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Input
|
<Input
|
||||||
@@ -631,9 +643,8 @@ export default function ChallengeManagement() {
|
|||||||
</Button>
|
</Button>
|
||||||
</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,116 +229,126 @@ 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}
|
||||||
{isCreating ? "Créer un événement" : "Modifier l'événement"}
|
onClose={handleCancel}
|
||||||
</h4>
|
size="lg"
|
||||||
<div className="space-y-4">
|
>
|
||||||
<Input
|
<div className="p-6">
|
||||||
type="date"
|
<div className="flex items-center justify-between mb-4">
|
||||||
label="Date"
|
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||||
value={formData.date}
|
{isCreating ? "Créer un événement" : "Modifier l'événement"}
|
||||||
onChange={(e) =>
|
</h4>
|
||||||
setFormData({ ...formData, date: e.target.value })
|
<CloseButton onClick={handleCancel} size="lg" />
|
||||||
}
|
</div>
|
||||||
className="text-xs sm:text-sm px-3 py-2"
|
<div className="space-y-4">
|
||||||
/>
|
<Input
|
||||||
<Input
|
type="date"
|
||||||
type="text"
|
label="Date"
|
||||||
label="Nom"
|
value={formData.date}
|
||||||
value={formData.name}
|
onChange={(e) =>
|
||||||
onChange={(e) =>
|
setFormData({ ...formData, date: e.target.value })
|
||||||
setFormData({ ...formData, name: e.target.value })
|
}
|
||||||
}
|
className="text-xs sm:text-sm px-3 py-2"
|
||||||
placeholder="Nom de l'événement"
|
/>
|
||||||
className="text-xs sm:text-sm px-3 py-2"
|
<Input
|
||||||
/>
|
type="text"
|
||||||
<Textarea
|
label="Nom"
|
||||||
label="Description"
|
value={formData.name}
|
||||||
value={formData.description}
|
onChange={(e) =>
|
||||||
onChange={(e) =>
|
setFormData({ ...formData, name: e.target.value })
|
||||||
setFormData({ ...formData, description: e.target.value })
|
}
|
||||||
}
|
placeholder="Nom de l'événement"
|
||||||
placeholder="Description de l'événement"
|
className="text-xs sm:text-sm px-3 py-2"
|
||||||
rows={4}
|
/>
|
||||||
className="text-xs sm:text-sm px-3 py-2"
|
<Textarea
|
||||||
/>
|
label="Description"
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
value={formData.description}
|
||||||
<div>
|
onChange={(e) =>
|
||||||
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
setFormData({ ...formData, description: e.target.value })
|
||||||
Type
|
}
|
||||||
</label>
|
placeholder="Description de l'événement"
|
||||||
<select
|
rows={4}
|
||||||
value={formData.type}
|
className="text-xs sm:text-sm px-3 py-2"
|
||||||
|
/>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-xs sm:text-sm text-gray-300 mb-1">
|
||||||
|
Type
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={formData.type}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
type: e.target.value as Event["type"],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
|
||||||
|
>
|
||||||
|
{eventTypes.map((type) => (
|
||||||
|
<option key={type} value={type}>
|
||||||
|
{getEventTypeLabel(type)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
label="Salle"
|
||||||
|
value={formData.room || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, room: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Ex: Nautilus"
|
||||||
|
className="text-xs sm:text-sm px-3 py-2"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
label="Heure"
|
||||||
|
value={formData.time || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
setFormData({ ...formData, time: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="Ex: 11h-12h"
|
||||||
|
className="text-xs sm:text-sm px-3 py-2"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
label="Places max"
|
||||||
|
value={formData.maxPlaces || ""}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
type: e.target.value as Event["type"],
|
maxPlaces: e.target.value
|
||||||
|
? parseInt(e.target.value)
|
||||||
|
: undefined,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
|
placeholder="Ex: 25"
|
||||||
|
className="text-xs sm:text-sm px-3 py-2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleSave}
|
||||||
|
variant="success"
|
||||||
|
size="md"
|
||||||
|
disabled={saving}
|
||||||
>
|
>
|
||||||
{eventTypes.map((type) => (
|
{saving ? "Enregistrement..." : "Enregistrer"}
|
||||||
<option key={type} value={type}>
|
</Button>
|
||||||
{getEventTypeLabel(type)}
|
<Button onClick={handleCancel} variant="secondary" size="md">
|
||||||
</option>
|
Annuler
|
||||||
))}
|
</Button>
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
label="Salle"
|
|
||||||
value={formData.room || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({ ...formData, room: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="Ex: Nautilus"
|
|
||||||
className="text-xs sm:text-sm px-3 py-2"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
label="Heure"
|
|
||||||
value={formData.time || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({ ...formData, time: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="Ex: 11h-12h"
|
|
||||||
className="text-xs sm:text-sm px-3 py-2"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
label="Places max"
|
|
||||||
value={formData.maxPlaces || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
setFormData({
|
|
||||||
...formData,
|
|
||||||
maxPlaces: e.target.value
|
|
||||||
? parseInt(e.target.value)
|
|
||||||
: undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
placeholder="Ex: 25"
|
|
||||||
className="text-xs sm:text-sm px-3 py-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col sm:flex-row gap-2">
|
|
||||||
<Button
|
|
||||||
onClick={handleSave}
|
|
||||||
variant="success"
|
|
||||||
size="md"
|
|
||||||
disabled={saving}
|
|
||||||
>
|
|
||||||
{saving ? "Enregistrement..." : "Enregistrer"}
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleCancel} variant="secondary" size="md">
|
|
||||||
Annuler
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Modal>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{events.length === 0 ? (
|
{events.length === 0 ? (
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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