Enhance event model and management: Add new fields for room, time, and maxPlaces to the Event model in Prisma schema. Update API routes and UI components to support these fields, improving event details and user interaction in event management and registration processes.
This commit is contained in:
@@ -7,8 +7,11 @@ interface Event {
|
||||
date: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: "SUMMIT" | "LAUNCH" | "FESTIVAL" | "COMPETITION";
|
||||
type: "SUMMIT" | "LAUNCH" | "FESTIVAL" | "COMPETITION" | "CODE_KATA";
|
||||
status: "UPCOMING" | "LIVE" | "PAST";
|
||||
room?: string | null;
|
||||
time?: string | null;
|
||||
maxPlaces?: number | null;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
registrationsCount?: number;
|
||||
@@ -18,8 +21,11 @@ interface EventFormData {
|
||||
date: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: "SUMMIT" | "LAUNCH" | "FESTIVAL" | "COMPETITION";
|
||||
type: "SUMMIT" | "LAUNCH" | "FESTIVAL" | "COMPETITION" | "CODE_KATA";
|
||||
status: "UPCOMING" | "LIVE" | "PAST";
|
||||
room?: string;
|
||||
time?: string;
|
||||
maxPlaces?: number;
|
||||
}
|
||||
|
||||
const eventTypes: Event["type"][] = [
|
||||
@@ -27,6 +33,7 @@ const eventTypes: Event["type"][] = [
|
||||
"LAUNCH",
|
||||
"FESTIVAL",
|
||||
"COMPETITION",
|
||||
"CODE_KATA",
|
||||
];
|
||||
const eventStatuses: Event["status"][] = ["UPCOMING", "LIVE", "PAST"];
|
||||
|
||||
@@ -40,6 +47,8 @@ const getEventTypeLabel = (type: Event["type"]) => {
|
||||
return "Festival";
|
||||
case "COMPETITION":
|
||||
return "Compétition";
|
||||
case "CODE_KATA":
|
||||
return "Code Kata";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
@@ -70,6 +79,9 @@ export default function EventManagement() {
|
||||
description: "",
|
||||
type: "SUMMIT",
|
||||
status: "UPCOMING",
|
||||
room: "",
|
||||
time: "",
|
||||
maxPlaces: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -99,6 +111,9 @@ export default function EventManagement() {
|
||||
description: "",
|
||||
type: "SUMMIT",
|
||||
status: "UPCOMING",
|
||||
room: "",
|
||||
time: "",
|
||||
maxPlaces: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -111,6 +126,9 @@ export default function EventManagement() {
|
||||
description: event.description,
|
||||
type: event.type,
|
||||
status: event.status,
|
||||
room: event.room || "",
|
||||
time: event.time || "",
|
||||
maxPlaces: event.maxPlaces || undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -146,6 +164,9 @@ export default function EventManagement() {
|
||||
description: "",
|
||||
type: "SUMMIT",
|
||||
status: "UPCOMING",
|
||||
room: "",
|
||||
time: "",
|
||||
maxPlaces: undefined,
|
||||
});
|
||||
} else {
|
||||
const error = await response?.json();
|
||||
@@ -190,6 +211,9 @@ export default function EventManagement() {
|
||||
description: "",
|
||||
type: "SUMMIT",
|
||||
status: "UPCOMING",
|
||||
room: "",
|
||||
time: "",
|
||||
maxPlaces: undefined,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -298,6 +322,55 @@ export default function EventManagement() {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-1">
|
||||
Salle
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.room || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, room: e.target.value })
|
||||
}
|
||||
placeholder="Ex: Nautilus"
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-1">
|
||||
Heure
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.time || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, time: e.target.value })
|
||||
}
|
||||
placeholder="Ex: 11h-12h"
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-1">
|
||||
Places max
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.maxPlaces || ""}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
maxPlaces: e.target.value
|
||||
? parseInt(e.target.value)
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
placeholder="Ex: 25"
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
@@ -352,10 +425,25 @@ export default function EventManagement() {
|
||||
<p className="text-gray-400 text-sm mb-2">
|
||||
{event.description}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 mt-2">
|
||||
<div className="flex flex-wrap items-center gap-4 mt-2">
|
||||
<p className="text-gray-500 text-xs">
|
||||
Date: {new Date(event.date).toLocaleDateString("fr-FR")}
|
||||
</p>
|
||||
{event.room && (
|
||||
<p className="text-gray-500 text-xs">
|
||||
📍 Salle: {event.room}
|
||||
</p>
|
||||
)}
|
||||
{event.time && (
|
||||
<p className="text-gray-500 text-xs">
|
||||
🕐 Heure: {event.time}
|
||||
</p>
|
||||
)}
|
||||
{event.maxPlaces && (
|
||||
<p className="text-gray-500 text-xs">
|
||||
👥 Places: {event.maxPlaces}
|
||||
</p>
|
||||
)}
|
||||
<span className="px-2 py-1 bg-blue-900/30 border border-blue-500/50 text-blue-400 text-xs rounded">
|
||||
{event.registrationsCount || 0} inscrit
|
||||
{event.registrationsCount !== 1 ? "s" : ""}
|
||||
|
||||
@@ -9,8 +9,11 @@ interface Event {
|
||||
date: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: "SUMMIT" | "LAUNCH" | "FESTIVAL" | "COMPETITION";
|
||||
type: "SUMMIT" | "LAUNCH" | "FESTIVAL" | "COMPETITION" | "CODE_KATA";
|
||||
status: "UPCOMING" | "LIVE" | "PAST";
|
||||
room?: string | null;
|
||||
time?: string | null;
|
||||
maxPlaces?: number | null;
|
||||
}
|
||||
|
||||
interface EventsPageSectionProps {
|
||||
@@ -29,6 +32,8 @@ const getEventTypeColor = (type: Event["type"]) => {
|
||||
return "from-pixel-gold to-orange-500";
|
||||
case "COMPETITION":
|
||||
return "from-red-600 to-orange-500";
|
||||
case "CODE_KATA":
|
||||
return "from-green-600 to-emerald-500";
|
||||
default:
|
||||
return "from-gray-600 to-gray-500";
|
||||
}
|
||||
@@ -44,6 +49,8 @@ const getEventTypeLabel = (type: Event["type"]) => {
|
||||
return "Festival";
|
||||
case "COMPETITION":
|
||||
return "Compétition";
|
||||
case "CODE_KATA":
|
||||
return "Code Kata";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
@@ -84,6 +91,7 @@ export default function EventsPageSection({
|
||||
const [loading, setLoading] = useState<Record<string, boolean>>({});
|
||||
const [error, setError] = useState<string>("");
|
||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
||||
|
||||
// Déterminer si on a des données initiales valides
|
||||
const hasInitialData = useMemo(
|
||||
@@ -338,10 +346,16 @@ export default function EventsPageSection({
|
||||
);
|
||||
};
|
||||
|
||||
const truncateDescription = (text: string, maxLength: number = 150) => {
|
||||
if (text.length <= maxLength) return text;
|
||||
return text.substring(0, maxLength).trim() + "...";
|
||||
};
|
||||
|
||||
const renderEventCard = (event: Event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm hover:border-pixel-gold/50 transition group"
|
||||
onClick={() => setSelectedEvent(event)}
|
||||
className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm hover:border-pixel-gold/50 transition group cursor-pointer"
|
||||
>
|
||||
{/* Event Header */}
|
||||
<div
|
||||
@@ -368,17 +382,52 @@ export default function EventsPageSection({
|
||||
{event.name}
|
||||
</h3>
|
||||
|
||||
{/* Description */}
|
||||
{/* Event Details */}
|
||||
{(event.room || event.time || event.maxPlaces) && (
|
||||
<div className="flex flex-wrap gap-3 mb-4 text-sm">
|
||||
{event.room && (
|
||||
<div className="flex items-center gap-1.5 text-gray-300">
|
||||
<span className="text-pixel-gold">📍</span>
|
||||
<span className="font-semibold">Salle:</span>
|
||||
<span>{event.room}</span>
|
||||
</div>
|
||||
)}
|
||||
{event.time && (
|
||||
<div className="flex items-center gap-1.5 text-gray-300">
|
||||
<span className="text-pixel-gold">🕐</span>
|
||||
<span className="font-semibold">Heure:</span>
|
||||
<span>{event.time}</span>
|
||||
</div>
|
||||
)}
|
||||
{event.maxPlaces && (
|
||||
<div className="flex items-center gap-1.5 text-gray-300">
|
||||
<span className="text-pixel-gold">👥</span>
|
||||
<span className="font-semibold">Places:</span>
|
||||
<span>{event.maxPlaces}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Description (truncated) */}
|
||||
<p className="text-gray-400 text-sm leading-relaxed mb-4">
|
||||
{event.description}
|
||||
{truncateDescription(event.description)}
|
||||
</p>
|
||||
{event.description.length > 150 && (
|
||||
<p className="text-pixel-gold text-xs mb-4 italic">
|
||||
Cliquez pour voir plus...
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Action Button */}
|
||||
{event.status === "UPCOMING" && (
|
||||
<>
|
||||
{registrations[event.id] ? (
|
||||
<button
|
||||
onClick={() => handleUnregister(event.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleUnregister(event.id);
|
||||
}}
|
||||
disabled={loading[event.id]}
|
||||
className="w-full px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
@@ -386,7 +435,10 @@ export default function EventsPageSection({
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleRegister(event.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRegister(event.id);
|
||||
}}
|
||||
disabled={loading[event.id]}
|
||||
className="w-full px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
@@ -557,6 +609,160 @@ export default function EventsPageSection({
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event Modal */}
|
||||
{selectedEvent && (
|
||||
<div
|
||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
>
|
||||
<div
|
||||
className="bg-black border-2 border-pixel-gold/70 rounded-lg max-w-3xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="p-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
{getStatusBadge(selectedEvent.status)}
|
||||
<span className="px-3 py-1 bg-pixel-gold/20 border border-pixel-gold/50 text-pixel-gold text-xs uppercase rounded">
|
||||
{getEventTypeLabel(selectedEvent.type)}
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-white uppercase tracking-wide">
|
||||
{selectedEvent.name}
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setSelectedEvent(null)}
|
||||
className="text-gray-400 hover:text-pixel-gold text-3xl font-bold transition ml-4"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Event Header Color Bar */}
|
||||
<div
|
||||
className={`h-1 bg-gradient-to-r ${getEventTypeColor(
|
||||
selectedEvent.type
|
||||
)} mb-6 rounded`}
|
||||
></div>
|
||||
|
||||
{/* Date */}
|
||||
<div className="text-white text-lg font-bold uppercase tracking-widest mb-4">
|
||||
{selectedEvent.date}
|
||||
</div>
|
||||
|
||||
{/* Event Details */}
|
||||
{(selectedEvent.room ||
|
||||
selectedEvent.time ||
|
||||
selectedEvent.maxPlaces) && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
||||
{selectedEvent.room && (
|
||||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||||
<span className="text-pixel-gold text-xl">📍</span>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
Salle
|
||||
</div>
|
||||
<div className="font-semibold">
|
||||
{selectedEvent.room}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.time && (
|
||||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||||
<span className="text-pixel-gold text-xl">🕐</span>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
Heure
|
||||
</div>
|
||||
<div className="font-semibold">
|
||||
{selectedEvent.time}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.maxPlaces && (
|
||||
<div className="flex items-center gap-2 text-gray-300 bg-black/40 p-3 rounded border border-pixel-gold/20">
|
||||
<span className="text-pixel-gold text-xl">👥</span>
|
||||
<div>
|
||||
<div className="text-xs text-gray-400 uppercase tracking-wider">
|
||||
Places
|
||||
</div>
|
||||
<div className="font-semibold">
|
||||
{selectedEvent.maxPlaces}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Full Description */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-pixel-gold text-sm uppercase tracking-widest mb-3">
|
||||
Description
|
||||
</h3>
|
||||
<p className="text-gray-300 text-sm leading-relaxed whitespace-pre-line">
|
||||
{selectedEvent.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
{selectedEvent.status === "UPCOMING" && (
|
||||
<div className="pt-4 border-t border-pixel-gold/20">
|
||||
{registrations[selectedEvent.id] ? (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleUnregister(selectedEvent.id);
|
||||
setSelectedEvent(null);
|
||||
}}
|
||||
disabled={loading[selectedEvent.id]}
|
||||
className="w-full px-4 py-3 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-sm tracking-widest rounded hover:bg-green-900/30 transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading[selectedEvent.id]
|
||||
? "Annulation..."
|
||||
: "Se désinscrire"}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRegister(selectedEvent.id);
|
||||
setSelectedEvent(null);
|
||||
}}
|
||||
disabled={loading[selectedEvent.id]}
|
||||
className="w-full px-4 py-3 border border-pixel-gold/50 bg-pixel-gold/10 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/20 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading[selectedEvent.id]
|
||||
? "Inscription..."
|
||||
: "S'inscrire maintenant"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.status === "LIVE" && (
|
||||
<div className="pt-4 border-t border-pixel-gold/20">
|
||||
<button className="w-full px-4 py-3 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-sm tracking-widest rounded hover:bg-red-900/30 transition animate-pulse">
|
||||
Rejoindre en direct
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{selectedEvent.status === "PAST" && (
|
||||
<div className="pt-4 border-t border-pixel-gold/20">
|
||||
<button className="w-full px-4 py-3 border border-gray-600/50 bg-gray-900/20 text-gray-500 uppercase text-sm tracking-widest rounded cursor-not-allowed">
|
||||
Événement terminé
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user