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:
Julien Froidefond
2025-12-16 16:50:06 +01:00
parent 16e4b63ffd
commit 79c21955e0
4 changed files with 720 additions and 662 deletions

View File

@@ -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>
); );

View File

@@ -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

View File

@@ -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;
} }