Compare commits

...

6 Commits

Author SHA1 Message Date
Julien Froidefond
5eddf36121 Refactor UserManagement component layout: Update user display to a grid format for improved responsiveness, enhance user information presentation with clearer stats and action buttons, and streamline the overall UI for better user experience.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m3s
2025-12-16 16:55:40 +01:00
Julien Froidefond
ec965cd59d Enhance ChallengeManagement and EventManagement components: Refactor layout for better readability, implement event registration viewing with score editing functionality, and improve user feedback handling in modals. Update EventRegistrationService to fetch event registrations with user details, ensuring a more interactive admin experience. 2025-12-16 16:52:50 +01:00
Julien Froidefond
79c21955e0 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. 2025-12-16 16:50:06 +01:00
Julien Froidefond
16e4b63ffd Add feedback management features: Implement functions to add bonus points and mark feedback as read in the FeedbackManagement component. Update EventFeedback model to include isRead property, enhancing user interaction and feedback tracking. 2025-12-16 16:43:53 +01:00
Julien Froidefond
3dd82c2bd4 Add event registration and feedback points to site preferences: Update SitePreferences model and related components to include eventRegistrationPoints and eventFeedbackPoints, ensuring proper handling of user scores during event interactions. 2025-12-16 16:38:01 +01:00
Julien Froidefond
f45cc1839e Add score property to UserData interface across navigation and profile components: Update Navigation.tsx, NavigationWrapper.tsx, and PlayerStats.tsx to include score, ensuring consistent user data handling and display. 2025-12-16 16:29:21 +01:00
32 changed files with 2250 additions and 971 deletions

127
actions/admin/feedback.ts Normal file
View File

@@ -0,0 +1,127 @@
"use server";
import { revalidatePath } from "next/cache";
import { auth } from "@/lib/auth";
import { prisma } from "@/services/database";
import { Role } from "@/prisma/generated/prisma/client";
import { NotFoundError } from "@/services/errors";
function checkAdminAccess() {
return async () => {
const session = await auth();
if (!session?.user || session.user.role !== Role.ADMIN) {
throw new Error("Accès refusé");
}
return session;
};
}
export async function addFeedbackBonusPoints(
userId: string,
points: number
) {
try {
await checkAdminAccess()();
// Vérifier que l'utilisateur existe
const user = await prisma.user.findUnique({
where: { id: userId },
select: { id: true, score: true },
});
if (!user) {
throw new NotFoundError("Utilisateur");
}
// Ajouter les points
const updatedUser = await prisma.user.update({
where: { id: userId },
data: {
score: {
increment: points,
},
},
select: {
id: true,
username: true,
score: true,
},
});
revalidatePath("/admin");
revalidatePath("/leaderboard");
return {
success: true,
message: `${points} points ajoutés avec succès`,
data: updatedUser,
};
} catch (error) {
console.error("Error adding bonus points:", error);
if (error instanceof NotFoundError) {
return { success: false, error: error.message };
}
if (error instanceof Error && error.message === "Accès refusé") {
return { success: false, error: "Accès refusé" };
}
return {
success: false,
error: "Erreur lors de l'ajout des points",
};
}
}
export async function markFeedbackAsRead(feedbackId: string, isRead: boolean) {
try {
await checkAdminAccess()();
// Vérifier que le feedback existe
const feedback = await prisma.eventFeedback.findUnique({
where: { id: feedbackId },
select: { id: true },
});
if (!feedback) {
throw new NotFoundError("Feedback");
}
// Mettre à jour le statut
const updatedFeedback = await prisma.eventFeedback.update({
where: { id: feedbackId },
data: {
isRead,
},
select: {
id: true,
isRead: true,
},
});
revalidatePath("/admin");
return {
success: true,
message: isRead
? "Feedback marqué comme lu"
: "Feedback marqué comme non lu",
data: updatedFeedback,
};
} catch (error) {
console.error("Error marking feedback as read:", error);
if (error instanceof NotFoundError) {
return { success: false, error: error.message };
}
if (error instanceof Error && error.message === "Accès refusé") {
return { success: false, error: "Accès refusé" };
}
return {
success: false,
error: "Erreur lors de la mise à jour du feedback",
};
}
}

View File

@@ -20,6 +20,8 @@ export async function updateSitePreferences(data: {
eventsBackground?: string | null;
leaderboardBackground?: string | null;
challengesBackground?: string | null;
eventRegistrationPoints?: number;
eventFeedbackPoints?: number;
}) {
try {
await checkAdminAccess()();
@@ -29,6 +31,8 @@ export async function updateSitePreferences(data: {
eventsBackground: data.eventsBackground,
leaderboardBackground: data.leaderboardBackground,
challengesBackground: data.challengesBackground,
eventRegistrationPoints: data.eventRegistrationPoints,
eventFeedbackPoints: data.eventFeedbackPoints,
});
revalidatePath("/admin");

View File

@@ -0,0 +1,31 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { eventRegistrationService } from "@/services/events/event-registration.service";
import { Role } from "@/prisma/generated/prisma/client";
export async function GET(
request: Request,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const session = await auth();
if (!session?.user || session.user.role !== Role.ADMIN) {
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
}
const { id: eventId } = await params;
const registrations = await eventRegistrationService.getEventRegistrations(
eventId
);
return NextResponse.json(registrations);
} catch (error) {
console.error("Error fetching event registrations:", error);
return NextResponse.json(
{ error: "Erreur lors de la récupération des inscrits" },
{ status: 500 }
);
}
}

View File

@@ -128,6 +128,9 @@ export default function FeedbackPageClient({
});
}
// Rafraîchir le score dans le header
window.dispatchEvent(new Event("refreshUserScore"));
// Rediriger après 2 secondes
setTimeout(() => {
router.push("/events");

View File

@@ -6,6 +6,8 @@ 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 EventPointsPreferences from "@/components/admin/EventPointsPreferences";
import EventFeedbackPointsPreferences from "@/components/admin/EventFeedbackPointsPreferences";
import { Button, Card, SectionTitle } from "@/components/ui";
interface SitePreferences {
@@ -14,6 +16,8 @@ interface SitePreferences {
eventsBackground: string | null;
leaderboardBackground: string | null;
challengesBackground: string | null;
eventRegistrationPoints: number;
eventFeedbackPoints: number;
}
interface AdminPanelProps {
@@ -93,6 +97,8 @@ export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
</div>
<div className="space-y-4">
<BackgroundPreferences initialPreferences={initialPreferences} />
<EventPointsPreferences initialPreferences={initialPreferences} />
<EventFeedbackPointsPreferences initialPreferences={initialPreferences} />
</div>
</Card>
)}

View File

@@ -11,6 +11,7 @@ interface SitePreferences {
eventsBackground: string | null;
leaderboardBackground: string | null;
challengesBackground: string | null;
eventRegistrationPoints?: number;
}
interface BackgroundPreferencesProps {

View File

@@ -9,7 +9,15 @@ import {
adminCancelChallenge,
reactivateChallenge,
} 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";
interface Challenge {
@@ -417,23 +425,29 @@ export default function ChallengeManagement() {
{/* Modal de validation */}
{selectedChallenge && (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
<Modal
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={() => {
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">
Désigner le gagnant
</h2>
size="lg"
/>
</div>
<div className="mb-6">
<h3 className="text-lg font-bold text-gray-300 mb-2">
@@ -545,30 +559,36 @@ export default function ChallengeManagement() {
</Button>
</div>
</div>
</Card>
</div>
</Modal>
)}
{/* Modal d'édition */}
{editingChallenge && (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm"
<Modal
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={() => {
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>
size="lg"
/>
</div>
<div className="space-y-4">
<Input
@@ -632,8 +652,7 @@ export default function ChallengeManagement() {
</div>
</div>
</div>
</Card>
</div>
</Modal>
)}
</div>
);

View File

@@ -0,0 +1,166 @@
"use client";
import { useState, useEffect } from "react";
import { updateSitePreferences } from "@/actions/admin/preferences";
import { Button, Card, Input } from "@/components/ui";
interface SitePreferences {
id: string;
eventFeedbackPoints: number;
}
interface EventFeedbackPointsPreferencesProps {
initialPreferences: SitePreferences;
}
export default function EventFeedbackPointsPreferences({
initialPreferences,
}: EventFeedbackPointsPreferencesProps) {
const [preferences, setPreferences] = useState<SitePreferences | null>(
initialPreferences
);
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState({
eventFeedbackPoints: initialPreferences.eventFeedbackPoints.toString(),
});
const [isSaving, setIsSaving] = useState(false);
// Synchroniser les préférences quand initialPreferences change
useEffect(() => {
setPreferences(initialPreferences);
setFormData({
eventFeedbackPoints: initialPreferences.eventFeedbackPoints.toString(),
});
}, [initialPreferences]);
const handleEdit = () => {
setIsEditing(true);
};
const handleSave = async () => {
const points = parseInt(formData.eventFeedbackPoints, 10);
if (isNaN(points) || points < 0) {
alert("Le nombre de points doit être un nombre positif");
return;
}
setIsSaving(true);
try {
const result = await updateSitePreferences({
eventFeedbackPoints: points,
});
if (result.success && result.data) {
setPreferences(result.data);
setFormData({
eventFeedbackPoints: result.data.eventFeedbackPoints.toString(),
});
setIsEditing(false);
} else {
console.error("Error updating preferences:", result.error);
alert(result.error || "Erreur lors de la mise à jour");
}
} catch (error) {
console.error("Error updating preferences:", error);
alert("Erreur lors de la mise à jour");
} finally {
setIsSaving(false);
}
};
const handleCancel = () => {
setIsEditing(false);
if (preferences) {
setFormData({
eventFeedbackPoints: preferences.eventFeedbackPoints.toString(),
});
}
};
return (
<Card variant="default" className="p-3 sm:p-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4">
<div className="min-w-0 flex-1">
<h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
Points de feedback sur les événements
</h3>
<p className="text-gray-400 text-xs sm:text-sm">
Nombre de points attribués lorsqu&apos;un utilisateur donne un feedback à un événement (première fois uniquement)
</p>
</div>
{!isEditing && (
<Button
onClick={handleEdit}
variant="primary"
size="sm"
className="whitespace-nowrap flex-shrink-0"
>
Modifier
</Button>
)}
</div>
{isEditing ? (
<div className="space-y-4">
<div>
<label
htmlFor="eventFeedbackPoints"
className="block text-sm font-medium text-pixel-gold mb-2"
>
Points de feedback
</label>
<Input
id="eventFeedbackPoints"
type="number"
min="0"
value={formData.eventFeedbackPoints}
onChange={(e) =>
setFormData({
...formData,
eventFeedbackPoints: e.target.value,
})
}
placeholder="100"
className="w-full"
/>
<p className="text-xs text-gray-400 mt-1">
Les utilisateurs gagneront ce nombre de points lors de leur premier feedback sur un événement
</p>
</div>
<div className="flex flex-col sm:flex-row gap-2 pt-4">
<Button
onClick={handleSave}
variant="success"
size="md"
disabled={isSaving}
>
{isSaving ? "Enregistrement..." : "Enregistrer"}
</Button>
<Button
onClick={handleCancel}
variant="secondary"
size="md"
disabled={isSaving}
>
Annuler
</Button>
</div>
</div>
) : (
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<span className="text-pixel-gold font-bold text-sm sm:text-base min-w-0 sm:min-w-[200px] flex-shrink-0">
Points actuels:
</span>
<div className="flex items-center gap-2">
<span className="text-lg sm:text-xl font-bold text-white">
{preferences?.eventFeedbackPoints ?? 100}
</span>
<span className="text-xs sm:text-sm text-gray-400">points</span>
</div>
</div>
)}
</Card>
);
}

View File

@@ -3,7 +3,17 @@
import { useState, useEffect, useTransition } from "react";
import { calculateEventStatus } from "@/lib/eventStatus";
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,
Avatar,
} from "@/components/ui";
import { updateUser } from "@/actions/admin/users";
interface Event {
id: string;
@@ -20,6 +30,24 @@ interface Event {
registrationsCount?: number;
}
interface EventRegistration {
id: string;
userId: string;
eventId: string;
createdAt: string;
user: {
id: string;
username: string;
avatar: string | null;
score: number;
level: number;
hp: number;
maxHp: number;
xp: number;
maxXp: number;
};
}
interface EventFormData {
date: string;
name: string;
@@ -70,6 +98,14 @@ export default function EventManagement() {
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
const [isCreating, setIsCreating] = useState(false);
const [saving, setSaving] = useState(false);
const [viewingRegistrations, setViewingRegistrations] =
useState<Event | null>(null);
const [registrations, setRegistrations] = useState<EventRegistration[]>([]);
const [loadingRegistrations, setLoadingRegistrations] = useState(false);
const [editingScores, setEditingScores] = useState<Record<string, number>>(
{}
);
const [savingScore, setSavingScore] = useState<string | null>(null);
const [formData, setFormData] = useState<EventFormData>({
date: "",
name: "",
@@ -199,6 +235,75 @@ export default function EventManagement() {
});
};
const handleViewRegistrations = async (event: Event) => {
setViewingRegistrations(event);
setLoadingRegistrations(true);
try {
const response = await fetch(
`/api/admin/events/${event.id}/registrations`
);
if (response.ok) {
const data = await response.json();
setRegistrations(data);
// Initialiser les scores d'édition avec les scores actuels
const scoresMap: Record<string, number> = {};
data.forEach((reg: EventRegistration) => {
scoresMap[reg.user.id] = reg.user.score;
});
setEditingScores(scoresMap);
} else {
alert("Erreur lors de la récupération des inscrits");
}
} catch (error) {
console.error("Error fetching registrations:", error);
alert("Erreur lors de la récupération des inscrits");
} finally {
setLoadingRegistrations(false);
}
};
const handleCloseRegistrations = () => {
setViewingRegistrations(null);
setRegistrations([]);
setEditingScores({});
};
const handleScoreChange = (userId: string, newScore: number) => {
setEditingScores({
...editingScores,
[userId]: newScore,
});
};
const handleSaveScore = async (userId: string) => {
const newScore = editingScores[userId];
if (newScore === undefined) return;
setSavingScore(userId);
startTransition(async () => {
try {
const result = await updateUser(userId, { score: newScore });
if (result.success) {
// Mettre à jour le score dans la liste locale
setRegistrations((prev) =>
prev.map((reg) =>
reg.user.id === userId
? { ...reg, user: { ...reg.user, score: newScore } }
: reg
)
);
} else {
alert(result.error || "Erreur lors de la mise à jour du score");
}
} catch (error) {
console.error("Error updating score:", error);
alert("Erreur lors de la mise à jour du score");
} finally {
setSavingScore(null);
}
});
};
if (loading) {
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
}
@@ -221,11 +326,20 @@ export default function EventManagement() {
)}
</div>
{/* Modal de création/édition */}
{(isCreating || editingEvent) && (
<Card variant="default" className="p-3 sm:p-4 mb-4">
<h4 className="text-pixel-gold font-bold mb-4 text-base sm:text-lg break-words">
<Modal
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"}
</h4>
<CloseButton onClick={handleCancel} size="lg" />
</div>
<div className="space-y-4">
<Input
type="date"
@@ -330,7 +444,8 @@ export default function EventManagement() {
</Button>
</div>
</div>
</Card>
</div>
</Modal>
)}
{events.length === 0 ? (
@@ -392,7 +507,15 @@ export default function EventManagement() {
</div>
</div>
{!isCreating && !editingEvent && (
<div className="flex gap-2 sm:ml-4 flex-shrink-0">
<div className="flex gap-2 sm:ml-4 flex-shrink-0 flex-wrap">
<Button
onClick={() => handleViewRegistrations(event)}
variant="primary"
size="sm"
className="whitespace-nowrap"
>
Inscrits ({event.registrationsCount || 0})
</Button>
<Button
onClick={() => handleEdit(event)}
variant="primary"
@@ -417,6 +540,116 @@ export default function EventManagement() {
})}
</div>
)}
{/* Modal des inscrits */}
{viewingRegistrations && (
<Modal
isOpen={!!viewingRegistrations}
onClose={handleCloseRegistrations}
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">
Inscrits à &quot;{viewingRegistrations.name}&quot;
</h4>
<CloseButton onClick={handleCloseRegistrations} size="lg" />
</div>
{loadingRegistrations ? (
<div className="text-center text-gray-400 py-8">
Chargement...
</div>
) : registrations.length === 0 ? (
<div className="text-center text-gray-400 py-8">
Aucun inscrit pour cet événement
</div>
) : (
<div className="space-y-3 max-h-[60vh] overflow-y-auto">
{registrations.map((registration) => {
const user = registration.user;
const currentScore = editingScores[user.id] ?? user.score;
const isSaving = savingScore === user.id;
return (
<Card
key={registration.id}
variant="default"
className="p-3 sm:p-4"
>
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<div className="flex items-center gap-3 flex-1 min-w-0">
<Avatar
src={user.avatar}
username={user.username}
size="md"
borderClassName="border-2 border-pixel-gold/50"
/>
<div className="flex-1 min-w-0">
<h5 className="text-pixel-gold font-bold text-sm sm:text-base break-words">
{user.username}
</h5>
<p className="text-gray-400 text-xs sm:text-sm">
Niveau {user.level} HP: {user.hp}/{user.maxHp}
XP: {user.xp}/{user.maxXp}
</p>
</div>
</div>
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-2 sm:gap-3 flex-shrink-0">
<div className="flex items-center gap-2">
<label className="text-xs sm:text-sm text-gray-300 whitespace-nowrap">
Score:
</label>
<input
type="number"
value={currentScore}
onChange={(e) =>
handleScoreChange(
user.id,
parseInt(e.target.value) || 0
)
}
disabled={isSaving}
className="w-24 px-2 sm:px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm text-center disabled:opacity-50"
/>
</div>
<div className="flex gap-1 sm:gap-2">
<button
onClick={() =>
handleScoreChange(user.id, currentScore - 100)
}
disabled={isSaving}
className="px-2 sm:px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-[10px] sm:text-xs rounded hover:bg-red-900/30 transition flex-shrink-0 disabled:opacity-50"
>
-100
</button>
<button
onClick={() =>
handleScoreChange(user.id, currentScore + 100)
}
disabled={isSaving}
className="px-2 sm:px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-[10px] sm:text-xs rounded hover:bg-green-900/30 transition flex-shrink-0 disabled:opacity-50"
>
+100
</button>
<button
onClick={() => handleSaveScore(user.id)}
disabled={isSaving || currentScore === user.score}
className="px-2 sm:px-3 py-1 border border-pixel-gold/50 bg-pixel-gold/20 text-pixel-gold text-[10px] sm:text-xs rounded hover:bg-pixel-gold/30 transition flex-shrink-0 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSaving ? "..." : "Sauver"}
</button>
</div>
</div>
</div>
</Card>
);
})}
</div>
)}
</div>
</Modal>
)}
</div>
);
}

View File

@@ -0,0 +1,166 @@
"use client";
import { useState, useEffect } from "react";
import { updateSitePreferences } from "@/actions/admin/preferences";
import { Button, Card, Input } from "@/components/ui";
interface SitePreferences {
id: string;
eventRegistrationPoints: number;
}
interface EventPointsPreferencesProps {
initialPreferences: SitePreferences;
}
export default function EventPointsPreferences({
initialPreferences,
}: EventPointsPreferencesProps) {
const [preferences, setPreferences] = useState<SitePreferences | null>(
initialPreferences
);
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState({
eventRegistrationPoints: initialPreferences.eventRegistrationPoints.toString(),
});
const [isSaving, setIsSaving] = useState(false);
// Synchroniser les préférences quand initialPreferences change
useEffect(() => {
setPreferences(initialPreferences);
setFormData({
eventRegistrationPoints: initialPreferences.eventRegistrationPoints.toString(),
});
}, [initialPreferences]);
const handleEdit = () => {
setIsEditing(true);
};
const handleSave = async () => {
const points = parseInt(formData.eventRegistrationPoints, 10);
if (isNaN(points) || points < 0) {
alert("Le nombre de points doit être un nombre positif");
return;
}
setIsSaving(true);
try {
const result = await updateSitePreferences({
eventRegistrationPoints: points,
});
if (result.success && result.data) {
setPreferences(result.data);
setFormData({
eventRegistrationPoints: result.data.eventRegistrationPoints.toString(),
});
setIsEditing(false);
} else {
console.error("Error updating preferences:", result.error);
alert(result.error || "Erreur lors de la mise à jour");
}
} catch (error) {
console.error("Error updating preferences:", error);
alert("Erreur lors de la mise à jour");
} finally {
setIsSaving(false);
}
};
const handleCancel = () => {
setIsEditing(false);
if (preferences) {
setFormData({
eventRegistrationPoints: preferences.eventRegistrationPoints.toString(),
});
}
};
return (
<Card variant="default" className="p-3 sm:p-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4">
<div className="min-w-0 flex-1">
<h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
Points d&apos;inscription aux événements
</h3>
<p className="text-gray-400 text-xs sm:text-sm">
Nombre de points attribués lorsqu&apos;un utilisateur s&apos;inscrit à un événement
</p>
</div>
{!isEditing && (
<Button
onClick={handleEdit}
variant="primary"
size="sm"
className="whitespace-nowrap flex-shrink-0"
>
Modifier
</Button>
)}
</div>
{isEditing ? (
<div className="space-y-4">
<div>
<label
htmlFor="eventRegistrationPoints"
className="block text-sm font-medium text-pixel-gold mb-2"
>
Points d&apos;inscription
</label>
<Input
id="eventRegistrationPoints"
type="number"
min="0"
value={formData.eventRegistrationPoints}
onChange={(e) =>
setFormData({
...formData,
eventRegistrationPoints: e.target.value,
})
}
placeholder="100"
className="w-full"
/>
<p className="text-xs text-gray-400 mt-1">
Les utilisateurs gagneront ce nombre de points lors de leur inscription à un événement
</p>
</div>
<div className="flex flex-col sm:flex-row gap-2 pt-4">
<Button
onClick={handleSave}
variant="success"
size="md"
disabled={isSaving}
>
{isSaving ? "Enregistrement..." : "Enregistrer"}
</Button>
<Button
onClick={handleCancel}
variant="secondary"
size="md"
disabled={isSaving}
>
Annuler
</Button>
</div>
</div>
) : (
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<span className="text-pixel-gold font-bold text-sm sm:text-base min-w-0 sm:min-w-[200px] flex-shrink-0">
Points actuels:
</span>
<div className="flex items-center gap-2">
<span className="text-lg sm:text-xl font-bold text-white">
{preferences?.eventRegistrationPoints ?? 100}
</span>
<span className="text-xs sm:text-sm text-gray-400">points</span>
</div>
</div>
)}
</Card>
);
}

View File

@@ -1,11 +1,18 @@
"use client";
import { useState, useEffect } from "react";
import {
addFeedbackBonusPoints,
markFeedbackAsRead,
} from "@/actions/admin/feedback";
import { Button } from "@/components/ui";
import Avatar from "@/components/ui/Avatar";
interface Feedback {
id: string;
rating: number;
comment: string | null;
isRead: boolean;
createdAt: string;
event: {
id: string;
@@ -17,6 +24,8 @@ interface Feedback {
id: string;
username: string;
email: string;
avatar: string | null;
score: number;
};
}
@@ -35,6 +44,10 @@ export default function FeedbackManagement() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [selectedEvent, setSelectedEvent] = useState<string | null>(null);
const [addingPoints, setAddingPoints] = useState<Record<string, boolean>>(
{}
);
const [markingRead, setMarkingRead] = useState<Record<string, boolean>>({});
useEffect(() => {
fetchFeedbacks();
@@ -92,9 +105,59 @@ export default function FeedbackManagement() {
);
};
const filteredFeedbacks = selectedEvent
const handleAddPoints = async (userId: string, points: number) => {
const key = `${userId}-${points}`;
setAddingPoints((prev) => ({ ...prev, [key]: true }));
setError("");
try {
const result = await addFeedbackBonusPoints(userId, points);
if (result.success) {
// Rafraîchir les données pour voir les nouveaux scores
await fetchFeedbacks();
// Rafraîchir le score dans le header si l'utilisateur est connecté
window.dispatchEvent(new Event("refreshUserScore"));
} else {
setError(result.error || "Erreur lors de l'ajout des points");
}
} catch {
setError("Erreur lors de l'ajout des points");
} finally {
setAddingPoints((prev) => ({ ...prev, [key]: false }));
}
};
const handleMarkAsRead = async (feedbackId: string, isRead: boolean) => {
setMarkingRead((prev) => ({ ...prev, [feedbackId]: true }));
setError("");
try {
const result = await markFeedbackAsRead(feedbackId, isRead);
if (result.success) {
// Rafraîchir les données pour voir le nouveau statut
await fetchFeedbacks();
} else {
setError(result.error || "Erreur lors de la mise à jour");
}
} catch {
setError("Erreur lors de la mise à jour");
} finally {
setMarkingRead((prev) => ({ ...prev, [feedbackId]: false }));
}
};
const filteredFeedbacks = (selectedEvent
? feedbacks.filter((f) => f.event.id === selectedEvent)
: feedbacks;
: feedbacks
).sort((a, b) => {
// Trier : non lus en premier, puis par date décroissante
if (a.isRead !== b.isRead) {
return a.isRead ? 1 : -1;
}
return (
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
});
if (loading) {
return (
@@ -184,21 +247,46 @@ export default function FeedbackManagement() {
{filteredFeedbacks.map((feedback) => (
<div
key={feedback.id}
className="bg-black/40 border border-pixel-gold/20 rounded p-3 sm:p-4"
className={`bg-black/40 border rounded p-3 sm:p-4 ${
feedback.isRead
? "border-pixel-gold/20 opacity-75"
: "border-pixel-gold/50 bg-pixel-gold/5"
}`}
>
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3 mb-3">
<div className="flex-1 min-w-0">
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-3 mb-2">
{/* En-tête utilisateur avec avatar */}
<div className="flex items-center gap-2 sm:gap-3 mb-3">
<Avatar
src={feedback.user.avatar}
username={feedback.user.username}
size="md"
borderClassName="border-pixel-gold/30"
/>
<div className="flex-1 min-w-0">
<div className="flex flex-col sm:flex-row sm:items-center gap-1 sm:gap-2 mb-1">
<h4 className="text-white font-semibold text-sm sm:text-base break-words">
{feedback.user.username}
</h4>
<span className="text-pixel-gold font-bold text-xs sm:text-sm">
{feedback.user.score.toLocaleString("fr-FR")} pts
</span>
</div>
<span className="text-gray-500 text-[10px] sm:text-xs break-all">
{feedback.user.email}
</span>
</div>
<div className="text-pixel-gold text-xs sm:text-sm font-semibold mb-2 break-words">
</div>
<div className="flex items-center gap-2 mb-2">
<div className="text-pixel-gold text-xs sm:text-sm font-semibold break-words">
{feedback.event.name}
</div>
{!feedback.isRead && (
<span className="bg-pixel-gold/20 text-pixel-gold text-[10px] px-1.5 py-0.5 rounded uppercase font-semibold">
Non lu
</span>
)}
</div>
<div className="text-gray-500 text-[10px] sm:text-xs mb-2">
{new Date(feedback.createdAt).toLocaleDateString(
"fr-FR",
@@ -212,8 +300,23 @@ export default function FeedbackManagement() {
)}
</div>
</div>
<div className="flex-shrink-0">
<div className="flex flex-col items-end gap-2 flex-shrink-0">
{renderStars(feedback.rating)}
<Button
variant={feedback.isRead ? "secondary" : "success"}
size="sm"
onClick={() =>
handleMarkAsRead(feedback.id, !feedback.isRead)
}
disabled={markingRead[feedback.id]}
className="text-xs whitespace-nowrap"
>
{markingRead[feedback.id]
? "..."
: feedback.isRead
? "Marquer non lu"
: "Marquer lu"}
</Button>
</div>
</div>
{feedback.comment && (
@@ -223,6 +326,39 @@ export default function FeedbackManagement() {
</p>
</div>
)}
{/* Boutons pour ajouter des points bonus */}
<div className="mt-3 pt-3 border-t border-pixel-gold/20 flex flex-wrap gap-2">
<span className="text-gray-400 text-xs sm:text-sm mr-2">
Points bonus:
</span>
<Button
variant="primary"
size="sm"
onClick={() => handleAddPoints(feedback.user.id, 10)}
disabled={addingPoints[`${feedback.user.id}-10`]}
className="text-xs"
>
{addingPoints[`${feedback.user.id}-10`] ? "..." : "+10"}
</Button>
<Button
variant="primary"
size="sm"
onClick={() => handleAddPoints(feedback.user.id, 100)}
disabled={addingPoints[`${feedback.user.id}-100`]}
className="text-xs"
>
{addingPoints[`${feedback.user.id}-100`] ? "..." : "+100"}
</Button>
<Button
variant="primary"
size="sm"
onClick={() => handleAddPoints(feedback.user.id, 1000)}
disabled={addingPoints[`${feedback.user.id}-1000`]}
className="text-xs"
>
{addingPoints[`${feedback.user.id}-1000`] ? "..." : "+1000"}
</Button>
</div>
</div>
))}
</div>

View File

@@ -1,7 +1,14 @@
"use client";
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";
interface User {
@@ -159,6 +166,25 @@ export default function UserManagement() {
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) {
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
}
@@ -170,79 +196,129 @@ export default function UserManagement() {
Aucun utilisateur trouvé
</div>
) : (
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;
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4">
{users.map((user) => {
return (
<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 gap-2 sm:gap-3 items-center flex-1 min-w-0">
{/* Avatar */}
<Card key={user.id} variant="default" className="p-3">
<div className="flex flex-col gap-2">
{/* Header avec avatar et nom */}
<div className="flex items-center gap-2">
<Avatar
src={displayAvatar}
username={displayUsername}
src={user.avatar}
username={user.username}
size="sm"
className="flex-shrink-0"
borderClassName="border-2 border-pixel-gold/50"
/>
<div className="flex-1 min-w-0">
<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">
{displayUsername}
<h3 className="text-pixel-gold font-bold text-sm truncate">
{user.username}
</h3>
<span className="text-[10px] sm:text-xs text-gray-500 whitespace-nowrap">
Niveau {user.level}
</span>
<span className="text-[10px] sm:text-xs text-gray-500 whitespace-nowrap">
Score: {formatNumber(user.score)}
<div className="flex items-center gap-1.5 mt-0.5">
<span className="text-[10px] text-gray-500">
Niv. {user.level}
</span>
<span
className={`text-[10px] sm:text-xs whitespace-nowrap ${
className={`text-[10px] font-bold px-1.5 py-0.5 rounded border ${
user.role === "ADMIN"
? "text-pixel-gold"
: "text-gray-500"
? "text-pixel-gold border-pixel-gold/50 bg-pixel-gold/10"
: "text-gray-500 border-gray-500/30 bg-gray-500/10"
}`}
>
{user.role}
</span>
</div>
<p className="text-gray-400 text-[10px] sm:text-xs truncate">
</div>
</div>
{/* Score en évidence */}
<div className="flex items-baseline gap-1.5 px-1">
<span className="text-[10px] text-gray-400">Score:</span>
<span className="text-lg font-bold text-pixel-gold">
{formatNumber(user.score)}
</span>
</div>
{/* Stats HP et XP */}
<div className="space-y-1.5 text-[10px]">
<div>
<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 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>
<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 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>
{/* Email */}
<p className="text-gray-400 text-[10px] truncate px-1">
{user.email}
</p>
</div>
</div>
{!isEditing && (
<div className="flex gap-2 flex-shrink-0 sm:ml-2">
{/* Boutons d'action */}
<div className="flex gap-2 pt-1">
<button
onClick={() => handleEdit(user)}
className="px-2 sm:px-3 py-1.5 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap"
className="flex-1 px-2 py-1 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] tracking-widest rounded hover:bg-pixel-gold/10 transition"
>
Modifier
</button>
<button
onClick={() => handleDelete(user.id)}
disabled={deletingUserId === user.id}
className="px-2 sm:px-3 py-1.5 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-red-900/30 transition disabled:opacity-50 whitespace-nowrap"
className="flex-1 px-2 py-1 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-[10px] tracking-widest rounded hover:bg-red-900/30 transition disabled:opacity-50"
>
{deletingUserId === user.id
? "Suppression..."
: "Supprimer"}
{deletingUserId === user.id ? "..." : "Suppr."}
</button>
</div>
)}
</div>
</Card>
);
})}
</div>
)}
{isEditing ? (
{/* 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&apos;utilisateur
</h4>
<CloseButton onClick={handleCancel} size="lg" />
</div>
<div className="space-y-4">
{/* Username Section */}
<Input
@@ -269,15 +345,15 @@ export default function UserManagement() {
<div className="relative">
<Avatar
src={editingUser.avatar}
username={editingUser.username || user.username}
username={
editingUser.username || currentEditingUserData.username
}
size="lg"
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="text-pixel-gold text-xs">
Upload...
</div>
<div className="text-pixel-gold text-xs">Upload...</div>
</div>
)}
</div>
@@ -330,7 +406,7 @@ export default function UserManagement() {
const file = e.target.files?.[0];
if (!file) return;
setUploadingAvatar(user.id);
setUploadingAvatar(editingUser.userId);
try {
const formData = new FormData();
formData.append("file", file);
@@ -363,16 +439,16 @@ export default function UserManagement() {
}
}}
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
variant="primary"
size="sm"
as="span"
className="cursor-pointer"
>
{uploadingAvatar === user.id
{uploadingAvatar === editingUser.userId
? "Upload en cours..."
: "Upload un avatar custom"}
</Button>
@@ -387,7 +463,7 @@ export default function UserManagement() {
Points de Vie (HP)
</label>
<span className="text-[10px] sm:text-xs text-gray-400">
{previewHp} / {user.maxHp}
{previewHp} / {currentEditingUserData.maxHp}
</span>
</div>
<div className="flex gap-1 sm:gap-2 flex-wrap">
@@ -453,7 +529,7 @@ export default function UserManagement() {
style={{
width: `${Math.min(
100,
(previewHp / user.maxHp) * 100
(previewHp / currentEditingUserData.maxHp) * 100
)}%`,
}}
/>
@@ -467,7 +543,8 @@ export default function UserManagement() {
Expérience (XP)
</label>
<span className="text-[10px] sm:text-xs text-gray-400">
{formatNumber(previewXp)} / {formatNumber(user.maxXp)}
{formatNumber(previewXp)} /{" "}
{formatNumber(currentEditingUserData.maxXp)}
</span>
</div>
<div className="flex gap-1 sm:gap-2 flex-wrap">
@@ -533,7 +610,7 @@ export default function UserManagement() {
style={{
width: `${Math.min(
100,
(previewXp / user.maxXp) * 100
(previewXp / currentEditingUserData.maxXp) * 100
)}%`,
}}
/>
@@ -695,60 +772,13 @@ export default function UserManagement() {
>
{saving ? "Enregistrement..." : "Enregistrer"}
</Button>
<Button
onClick={handleCancel}
variant="secondary"
size="md"
>
<Button onClick={handleCancel} variant="secondary" size="md">
Annuler
</Button>
</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 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>
)}
</div>
);

View File

@@ -564,6 +564,8 @@ export default function EventsPageSection({
...prev,
[eventId]: true,
}));
// Rafraîchir le score dans le header
window.dispatchEvent(new Event("refreshUserScore"));
} else {
setError(result.error || "Une erreur est survenue");
}
@@ -583,6 +585,8 @@ export default function EventsPageSection({
...prev,
[eventId]: false,
}));
// Rafraîchir le score dans le header
window.dispatchEvent(new Event("refreshUserScore"));
} else {
setError(result.error || "Une erreur est survenue");
}

View File

@@ -155,6 +155,9 @@ export default function FeedbackModal({
});
}
// Rafraîchir le score dans le header
window.dispatchEvent(new Event("refreshUserScore"));
// Fermer la modale après 1.5 secondes
setTimeout(() => {
onClose();

View File

@@ -16,6 +16,7 @@ interface UserData {
xp: number;
maxXp: number;
level: number;
score: number;
}
interface NavigationProps {

View File

@@ -11,6 +11,7 @@ interface UserData {
xp: number;
maxXp: number;
level: number;
score: number;
}
export default async function NavigationWrapper() {
@@ -31,6 +32,7 @@ export default async function NavigationWrapper() {
xp: true,
maxXp: true,
level: true,
score: true,
}),
challengeService.getActiveChallengesCount(session.user.id),
]);

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useState, useCallback } from "react";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { Avatar } from "@/components/ui";
@@ -13,6 +13,7 @@ interface UserData {
xp: number;
maxXp: number;
level: number;
score: number;
}
interface PlayerStatsProps {
@@ -32,6 +33,7 @@ const defaultUserData: UserData = {
xp: 0,
maxXp: 5000,
level: 1,
score: 0,
};
export default function PlayerStats({ initialUserData }: PlayerStatsProps) {
@@ -40,6 +42,31 @@ export default function PlayerStats({ initialUserData }: PlayerStatsProps) {
initialUserData || defaultUserData
);
const refreshUserData = useCallback(async () => {
if (!session?.user?.id) return;
try {
const res = await fetch(`/api/users/${session.user.id}`);
const data = await res.json();
if (data) {
requestAnimationFrame(() => {
setUserData({
username: data.username || "Guest",
avatar: data.avatar,
hp: data.hp || 1000,
maxHp: data.maxHp || 1000,
xp: data.xp || 0,
maxXp: data.maxXp || 5000,
level: data.level || 1,
score: data.score || 0,
});
});
}
} catch (error) {
console.error("Error refreshing user data:", error);
}
}, [session]);
useEffect(() => {
// Si on a déjà des données initiales, ne rien faire (déjà initialisé dans useState)
if (initialUserData) {
@@ -62,6 +89,7 @@ export default function PlayerStats({ initialUserData }: PlayerStatsProps) {
xp: data.xp || 0,
maxXp: data.maxXp || 5000,
level: data.level || 1,
score: data.score || 0,
});
});
}
@@ -77,6 +105,7 @@ export default function PlayerStats({ initialUserData }: PlayerStatsProps) {
xp: 0,
maxXp: 5000,
level: 1,
score: 0,
});
});
});
@@ -88,51 +117,19 @@ export default function PlayerStats({ initialUserData }: PlayerStatsProps) {
}
}, [session, initialUserData]);
const { username, avatar, hp, maxHp, xp, maxXp, level } = userData;
// Calculer les pourcentages cibles
const targetHpPercentage = (hp / maxHp) * 100;
const targetXpPercentage = (xp / maxXp) * 100;
// Initialiser les pourcentages à 0 si on a des données initiales (pour l'animation)
// Sinon utiliser directement les valeurs calculées
const [hpPercentage, setHpPercentage] = useState(
initialUserData ? 0 : targetHpPercentage
);
const [xpPercentage, setXpPercentage] = useState(
initialUserData ? 0 : targetXpPercentage
);
// Écouter les événements de refresh du score
useEffect(() => {
// Si on a des données initiales, animer depuis 0 vers la valeur cible
if (initialUserData) {
const hpTimer = setTimeout(() => {
setHpPercentage(targetHpPercentage);
}, 100);
const xpTimer = setTimeout(() => {
setXpPercentage(targetXpPercentage);
}, 200);
return () => {
clearTimeout(hpTimer);
clearTimeout(xpTimer);
const handleRefreshScore = () => {
refreshUserData();
};
}
// Sinon, mettre à jour directement (pour les pages Client Components)
// Utiliser requestAnimationFrame pour éviter les cascades de rendu
requestAnimationFrame(() => {
setHpPercentage(targetHpPercentage);
setXpPercentage(targetXpPercentage);
});
}, [targetHpPercentage, targetXpPercentage, initialUserData]);
const hpColor =
hpPercentage > 60
? "from-green-600 to-green-700"
: hpPercentage > 30
? "from-yellow-600 to-orange-700"
: "from-red-700 to-red-900";
window.addEventListener("refreshUserScore", handleRefreshScore);
return () => {
window.removeEventListener("refreshUserScore", handleRefreshScore);
};
}, [refreshUserData]);
const { username, avatar, level, score } = userData;
return (
<div className="flex items-center gap-3">
@@ -150,7 +147,7 @@ export default function PlayerStats({ initialUserData }: PlayerStatsProps) {
</Link>
{/* Stats */}
<div className="flex flex-col gap-1.5 min-w-[180px] sm:min-w-[200px]">
<div className="flex flex-col gap-1.5 min-w-[140px] sm:min-w-[160px]">
{/* Username & Level */}
<div className="flex items-center gap-2">
<Link
@@ -166,57 +163,16 @@ export default function PlayerStats({ initialUserData }: PlayerStatsProps) {
</div>
</div>
{/* Bars side by side */}
<div className="flex flex-col gap-1">
{/* Score Display */}
<div className="flex items-center gap-2">
{/* HP Bar */}
<div className="relative h-2 flex-1 bg-gray-900 border border-gray-700 rounded overflow-hidden">
<div
className={`absolute inset-0 bg-gradient-to-r ${hpColor} transition-all duration-1000 ease-out`}
style={{ width: `${hpPercentage}%` }}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shimmer"></div>
<div className="text-gray-400 font-pixel text-xs uppercase">
Score
</div>
{hpPercentage < 30 && (
<div className="absolute inset-0 border border-red-500 rounded animate-pulse"></div>
)}
</div>
{/* XP Bar */}
<div className="relative h-2 flex-1 bg-gray-900 border border-pixel-gold/30 rounded overflow-hidden">
<div
className="absolute inset-0 bg-gradient-to-r from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80 transition-all duration-1000 ease-out"
style={{ width: `${xpPercentage}%` }}
>
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shimmer"></div>
<div className="text-pixel-gold font-gaming font-bold text-sm">
{formatNumber(score)}
</div>
</div>
</div>
{/* Labels */}
<div className="flex items-center gap-2 text-[8px] font-pixel text-gray-400">
<div className="flex-1 text-left">
HP {hp} / {maxHp}
</div>
<div className="flex-1 text-right">
XP {formatNumber(xp)} / {formatNumber(maxXp)}
</div>
</div>
</div>
</div>
<style jsx>{`
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
.animate-shimmer {
animation: shimmer 2s infinite;
}
`}</style>
</div>
);
}

View File

@@ -1,6 +1,7 @@
"use client";
import { ReactNode, useEffect } from "react";
import { createPortal } from "react-dom";
interface ModalProps {
isOpen: boolean;
@@ -37,7 +38,7 @@ export default function Modal({
if (!isOpen) return null;
return (
const modalContent = (
<div
className="fixed inset-0 z-[200] flex items-center justify-center p-4 backdrop-blur-sm"
style={{
@@ -59,4 +60,11 @@ export default function Modal({
</div>
</div>
);
// Utiliser un portal pour rendre le modal directement dans le body
if (typeof window !== "undefined") {
return createPortal(modalContent, document.body);
}
return null;
}

View File

@@ -211,6 +211,19 @@ export type IntNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedIntNullableFilter<$PrismaModel>
}
export type BoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type EnumChallengeStatusFilter<$PrismaModel = never> = {
equals?: $Enums.ChallengeStatus | Prisma.EnumChallengeStatusFieldRefInput<$PrismaModel>
in?: $Enums.ChallengeStatus[]
@@ -467,6 +480,19 @@ export type NestedFloatNullableFilter<$PrismaModel = never> = {
not?: Prisma.NestedFloatNullableFilter<$PrismaModel> | number | null
}
export type NestedBoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type NestedEnumChallengeStatusFilter<$PrismaModel = never> = {
equals?: $Enums.ChallengeStatus | Prisma.EnumChallengeStatusFieldRefInput<$PrismaModel>
in?: $Enums.ChallengeStatus[]

File diff suppressed because one or more lines are too long

View File

@@ -1032,6 +1032,7 @@ export const EventFeedbackScalarFieldEnum = {
eventId: 'eventId',
rating: 'rating',
comment: 'comment',
isRead: 'isRead',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
@@ -1045,6 +1046,8 @@ export const SitePreferencesScalarFieldEnum = {
eventsBackground: 'eventsBackground',
leaderboardBackground: 'leaderboardBackground',
challengesBackground: 'challengesBackground',
eventRegistrationPoints: 'eventRegistrationPoints',
eventFeedbackPoints: 'eventFeedbackPoints',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
@@ -1136,6 +1139,13 @@ export type EnumEventTypeFieldRefInput<$PrismaModel> = FieldRefInputType<$Prisma
/**
* Reference to a field of type 'Boolean'
*/
export type BooleanFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Boolean'>
/**
* Reference to a field of type 'ChallengeStatus'
*/

View File

@@ -141,6 +141,7 @@ export const EventFeedbackScalarFieldEnum = {
eventId: 'eventId',
rating: 'rating',
comment: 'comment',
isRead: 'isRead',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
@@ -154,6 +155,8 @@ export const SitePreferencesScalarFieldEnum = {
eventsBackground: 'eventsBackground',
leaderboardBackground: 'leaderboardBackground',
challengesBackground: 'challengesBackground',
eventRegistrationPoints: 'eventRegistrationPoints',
eventFeedbackPoints: 'eventFeedbackPoints',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const

View File

@@ -40,6 +40,7 @@ export type EventFeedbackMinAggregateOutputType = {
eventId: string | null
rating: number | null
comment: string | null
isRead: boolean | null
createdAt: Date | null
updatedAt: Date | null
}
@@ -50,6 +51,7 @@ export type EventFeedbackMaxAggregateOutputType = {
eventId: string | null
rating: number | null
comment: string | null
isRead: boolean | null
createdAt: Date | null
updatedAt: Date | null
}
@@ -60,6 +62,7 @@ export type EventFeedbackCountAggregateOutputType = {
eventId: number
rating: number
comment: number
isRead: number
createdAt: number
updatedAt: number
_all: number
@@ -80,6 +83,7 @@ export type EventFeedbackMinAggregateInputType = {
eventId?: true
rating?: true
comment?: true
isRead?: true
createdAt?: true
updatedAt?: true
}
@@ -90,6 +94,7 @@ export type EventFeedbackMaxAggregateInputType = {
eventId?: true
rating?: true
comment?: true
isRead?: true
createdAt?: true
updatedAt?: true
}
@@ -100,6 +105,7 @@ export type EventFeedbackCountAggregateInputType = {
eventId?: true
rating?: true
comment?: true
isRead?: true
createdAt?: true
updatedAt?: true
_all?: true
@@ -197,6 +203,7 @@ export type EventFeedbackGroupByOutputType = {
eventId: string
rating: number
comment: string | null
isRead: boolean
createdAt: Date
updatedAt: Date
_count: EventFeedbackCountAggregateOutputType | null
@@ -230,6 +237,7 @@ export type EventFeedbackWhereInput = {
eventId?: Prisma.StringFilter<"EventFeedback"> | string
rating?: Prisma.IntFilter<"EventFeedback"> | number
comment?: Prisma.StringNullableFilter<"EventFeedback"> | string | null
isRead?: Prisma.BoolFilter<"EventFeedback"> | boolean
createdAt?: Prisma.DateTimeFilter<"EventFeedback"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"EventFeedback"> | Date | string
event?: Prisma.XOR<Prisma.EventScalarRelationFilter, Prisma.EventWhereInput>
@@ -242,6 +250,7 @@ export type EventFeedbackOrderByWithRelationInput = {
eventId?: Prisma.SortOrder
rating?: Prisma.SortOrder
comment?: Prisma.SortOrderInput | Prisma.SortOrder
isRead?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
event?: Prisma.EventOrderByWithRelationInput
@@ -258,6 +267,7 @@ export type EventFeedbackWhereUniqueInput = Prisma.AtLeast<{
eventId?: Prisma.StringFilter<"EventFeedback"> | string
rating?: Prisma.IntFilter<"EventFeedback"> | number
comment?: Prisma.StringNullableFilter<"EventFeedback"> | string | null
isRead?: Prisma.BoolFilter<"EventFeedback"> | boolean
createdAt?: Prisma.DateTimeFilter<"EventFeedback"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"EventFeedback"> | Date | string
event?: Prisma.XOR<Prisma.EventScalarRelationFilter, Prisma.EventWhereInput>
@@ -270,6 +280,7 @@ export type EventFeedbackOrderByWithAggregationInput = {
eventId?: Prisma.SortOrder
rating?: Prisma.SortOrder
comment?: Prisma.SortOrderInput | Prisma.SortOrder
isRead?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_count?: Prisma.EventFeedbackCountOrderByAggregateInput
@@ -288,6 +299,7 @@ export type EventFeedbackScalarWhereWithAggregatesInput = {
eventId?: Prisma.StringWithAggregatesFilter<"EventFeedback"> | string
rating?: Prisma.IntWithAggregatesFilter<"EventFeedback"> | number
comment?: Prisma.StringNullableWithAggregatesFilter<"EventFeedback"> | string | null
isRead?: Prisma.BoolWithAggregatesFilter<"EventFeedback"> | boolean
createdAt?: Prisma.DateTimeWithAggregatesFilter<"EventFeedback"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"EventFeedback"> | Date | string
}
@@ -296,6 +308,7 @@ export type EventFeedbackCreateInput = {
id?: string
rating: number
comment?: string | null
isRead?: boolean
createdAt?: Date | string
updatedAt?: Date | string
event: Prisma.EventCreateNestedOneWithoutFeedbacksInput
@@ -308,6 +321,7 @@ export type EventFeedbackUncheckedCreateInput = {
eventId: string
rating: number
comment?: string | null
isRead?: boolean
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -316,6 +330,7 @@ export type EventFeedbackUpdateInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
rating?: Prisma.IntFieldUpdateOperationsInput | number
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
isRead?: Prisma.BoolFieldUpdateOperationsInput | boolean
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
event?: Prisma.EventUpdateOneRequiredWithoutFeedbacksNestedInput
@@ -328,6 +343,7 @@ export type EventFeedbackUncheckedUpdateInput = {
eventId?: Prisma.StringFieldUpdateOperationsInput | string
rating?: Prisma.IntFieldUpdateOperationsInput | number
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
isRead?: Prisma.BoolFieldUpdateOperationsInput | boolean
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -338,6 +354,7 @@ export type EventFeedbackCreateManyInput = {
eventId: string
rating: number
comment?: string | null
isRead?: boolean
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -346,6 +363,7 @@ export type EventFeedbackUpdateManyMutationInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
rating?: Prisma.IntFieldUpdateOperationsInput | number
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
isRead?: Prisma.BoolFieldUpdateOperationsInput | boolean
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -356,6 +374,7 @@ export type EventFeedbackUncheckedUpdateManyInput = {
eventId?: Prisma.StringFieldUpdateOperationsInput | string
rating?: Prisma.IntFieldUpdateOperationsInput | number
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
isRead?: Prisma.BoolFieldUpdateOperationsInput | boolean
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -381,6 +400,7 @@ export type EventFeedbackCountOrderByAggregateInput = {
eventId?: Prisma.SortOrder
rating?: Prisma.SortOrder
comment?: Prisma.SortOrder
isRead?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -395,6 +415,7 @@ export type EventFeedbackMaxOrderByAggregateInput = {
eventId?: Prisma.SortOrder
rating?: Prisma.SortOrder
comment?: Prisma.SortOrder
isRead?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -405,6 +426,7 @@ export type EventFeedbackMinOrderByAggregateInput = {
eventId?: Prisma.SortOrder
rating?: Prisma.SortOrder
comment?: Prisma.SortOrder
isRead?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -497,10 +519,15 @@ export type EventFeedbackUncheckedUpdateManyWithoutEventNestedInput = {
deleteMany?: Prisma.EventFeedbackScalarWhereInput | Prisma.EventFeedbackScalarWhereInput[]
}
export type BoolFieldUpdateOperationsInput = {
set?: boolean
}
export type EventFeedbackCreateWithoutUserInput = {
id?: string
rating: number
comment?: string | null
isRead?: boolean
createdAt?: Date | string
updatedAt?: Date | string
event: Prisma.EventCreateNestedOneWithoutFeedbacksInput
@@ -511,6 +538,7 @@ export type EventFeedbackUncheckedCreateWithoutUserInput = {
eventId: string
rating: number
comment?: string | null
isRead?: boolean
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -549,6 +577,7 @@ export type EventFeedbackScalarWhereInput = {
eventId?: Prisma.StringFilter<"EventFeedback"> | string
rating?: Prisma.IntFilter<"EventFeedback"> | number
comment?: Prisma.StringNullableFilter<"EventFeedback"> | string | null
isRead?: Prisma.BoolFilter<"EventFeedback"> | boolean
createdAt?: Prisma.DateTimeFilter<"EventFeedback"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"EventFeedback"> | Date | string
}
@@ -557,6 +586,7 @@ export type EventFeedbackCreateWithoutEventInput = {
id?: string
rating: number
comment?: string | null
isRead?: boolean
createdAt?: Date | string
updatedAt?: Date | string
user: Prisma.UserCreateNestedOneWithoutEventFeedbacksInput
@@ -567,6 +597,7 @@ export type EventFeedbackUncheckedCreateWithoutEventInput = {
userId: string
rating: number
comment?: string | null
isRead?: boolean
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -601,6 +632,7 @@ export type EventFeedbackCreateManyUserInput = {
eventId: string
rating: number
comment?: string | null
isRead?: boolean
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -609,6 +641,7 @@ export type EventFeedbackUpdateWithoutUserInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
rating?: Prisma.IntFieldUpdateOperationsInput | number
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
isRead?: Prisma.BoolFieldUpdateOperationsInput | boolean
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
event?: Prisma.EventUpdateOneRequiredWithoutFeedbacksNestedInput
@@ -619,6 +652,7 @@ export type EventFeedbackUncheckedUpdateWithoutUserInput = {
eventId?: Prisma.StringFieldUpdateOperationsInput | string
rating?: Prisma.IntFieldUpdateOperationsInput | number
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
isRead?: Prisma.BoolFieldUpdateOperationsInput | boolean
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -628,6 +662,7 @@ export type EventFeedbackUncheckedUpdateManyWithoutUserInput = {
eventId?: Prisma.StringFieldUpdateOperationsInput | string
rating?: Prisma.IntFieldUpdateOperationsInput | number
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
isRead?: Prisma.BoolFieldUpdateOperationsInput | boolean
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -637,6 +672,7 @@ export type EventFeedbackCreateManyEventInput = {
userId: string
rating: number
comment?: string | null
isRead?: boolean
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -645,6 +681,7 @@ export type EventFeedbackUpdateWithoutEventInput = {
id?: Prisma.StringFieldUpdateOperationsInput | string
rating?: Prisma.IntFieldUpdateOperationsInput | number
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
isRead?: Prisma.BoolFieldUpdateOperationsInput | boolean
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
user?: Prisma.UserUpdateOneRequiredWithoutEventFeedbacksNestedInput
@@ -655,6 +692,7 @@ export type EventFeedbackUncheckedUpdateWithoutEventInput = {
userId?: Prisma.StringFieldUpdateOperationsInput | string
rating?: Prisma.IntFieldUpdateOperationsInput | number
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
isRead?: Prisma.BoolFieldUpdateOperationsInput | boolean
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -664,6 +702,7 @@ export type EventFeedbackUncheckedUpdateManyWithoutEventInput = {
userId?: Prisma.StringFieldUpdateOperationsInput | string
rating?: Prisma.IntFieldUpdateOperationsInput | number
comment?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
isRead?: Prisma.BoolFieldUpdateOperationsInput | boolean
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -676,6 +715,7 @@ export type EventFeedbackSelect<ExtArgs extends runtime.Types.Extensions.Interna
eventId?: boolean
rating?: boolean
comment?: boolean
isRead?: boolean
createdAt?: boolean
updatedAt?: boolean
event?: boolean | Prisma.EventDefaultArgs<ExtArgs>
@@ -688,6 +728,7 @@ export type EventFeedbackSelectCreateManyAndReturn<ExtArgs extends runtime.Types
eventId?: boolean
rating?: boolean
comment?: boolean
isRead?: boolean
createdAt?: boolean
updatedAt?: boolean
event?: boolean | Prisma.EventDefaultArgs<ExtArgs>
@@ -700,6 +741,7 @@ export type EventFeedbackSelectUpdateManyAndReturn<ExtArgs extends runtime.Types
eventId?: boolean
rating?: boolean
comment?: boolean
isRead?: boolean
createdAt?: boolean
updatedAt?: boolean
event?: boolean | Prisma.EventDefaultArgs<ExtArgs>
@@ -712,11 +754,12 @@ export type EventFeedbackSelectScalar = {
eventId?: boolean
rating?: boolean
comment?: boolean
isRead?: boolean
createdAt?: boolean
updatedAt?: boolean
}
export type EventFeedbackOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "eventId" | "rating" | "comment" | "createdAt" | "updatedAt", ExtArgs["result"]["eventFeedback"]>
export type EventFeedbackOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "userId" | "eventId" | "rating" | "comment" | "isRead" | "createdAt" | "updatedAt", ExtArgs["result"]["eventFeedback"]>
export type EventFeedbackInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
event?: boolean | Prisma.EventDefaultArgs<ExtArgs>
user?: boolean | Prisma.UserDefaultArgs<ExtArgs>
@@ -742,6 +785,7 @@ export type $EventFeedbackPayload<ExtArgs extends runtime.Types.Extensions.Inter
eventId: string
rating: number
comment: string | null
isRead: boolean
createdAt: Date
updatedAt: Date
}, ExtArgs["result"]["eventFeedback"]>
@@ -1174,6 +1218,7 @@ export interface EventFeedbackFieldRefs {
readonly eventId: Prisma.FieldRef<"EventFeedback", 'String'>
readonly rating: Prisma.FieldRef<"EventFeedback", 'Int'>
readonly comment: Prisma.FieldRef<"EventFeedback", 'String'>
readonly isRead: Prisma.FieldRef<"EventFeedback", 'Boolean'>
readonly createdAt: Prisma.FieldRef<"EventFeedback", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"EventFeedback", 'DateTime'>
}

View File

@@ -20,16 +20,30 @@ export type SitePreferencesModel = runtime.Types.Result.DefaultSelection<Prisma.
export type AggregateSitePreferences = {
_count: SitePreferencesCountAggregateOutputType | null
_avg: SitePreferencesAvgAggregateOutputType | null
_sum: SitePreferencesSumAggregateOutputType | null
_min: SitePreferencesMinAggregateOutputType | null
_max: SitePreferencesMaxAggregateOutputType | null
}
export type SitePreferencesAvgAggregateOutputType = {
eventRegistrationPoints: number | null
eventFeedbackPoints: number | null
}
export type SitePreferencesSumAggregateOutputType = {
eventRegistrationPoints: number | null
eventFeedbackPoints: number | null
}
export type SitePreferencesMinAggregateOutputType = {
id: string | null
homeBackground: string | null
eventsBackground: string | null
leaderboardBackground: string | null
challengesBackground: string | null
eventRegistrationPoints: number | null
eventFeedbackPoints: number | null
createdAt: Date | null
updatedAt: Date | null
}
@@ -40,6 +54,8 @@ export type SitePreferencesMaxAggregateOutputType = {
eventsBackground: string | null
leaderboardBackground: string | null
challengesBackground: string | null
eventRegistrationPoints: number | null
eventFeedbackPoints: number | null
createdAt: Date | null
updatedAt: Date | null
}
@@ -50,18 +66,32 @@ export type SitePreferencesCountAggregateOutputType = {
eventsBackground: number
leaderboardBackground: number
challengesBackground: number
eventRegistrationPoints: number
eventFeedbackPoints: number
createdAt: number
updatedAt: number
_all: number
}
export type SitePreferencesAvgAggregateInputType = {
eventRegistrationPoints?: true
eventFeedbackPoints?: true
}
export type SitePreferencesSumAggregateInputType = {
eventRegistrationPoints?: true
eventFeedbackPoints?: true
}
export type SitePreferencesMinAggregateInputType = {
id?: true
homeBackground?: true
eventsBackground?: true
leaderboardBackground?: true
challengesBackground?: true
eventRegistrationPoints?: true
eventFeedbackPoints?: true
createdAt?: true
updatedAt?: true
}
@@ -72,6 +102,8 @@ export type SitePreferencesMaxAggregateInputType = {
eventsBackground?: true
leaderboardBackground?: true
challengesBackground?: true
eventRegistrationPoints?: true
eventFeedbackPoints?: true
createdAt?: true
updatedAt?: true
}
@@ -82,6 +114,8 @@ export type SitePreferencesCountAggregateInputType = {
eventsBackground?: true
leaderboardBackground?: true
challengesBackground?: true
eventRegistrationPoints?: true
eventFeedbackPoints?: true
createdAt?: true
updatedAt?: true
_all?: true
@@ -122,6 +156,18 @@ export type SitePreferencesAggregateArgs<ExtArgs extends runtime.Types.Extension
* Count returned SitePreferences
**/
_count?: true | SitePreferencesCountAggregateInputType
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs}
*
* Select which fields to average
**/
_avg?: SitePreferencesAvgAggregateInputType
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs}
*
* Select which fields to sum
**/
_sum?: SitePreferencesSumAggregateInputType
/**
* {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs}
*
@@ -155,6 +201,8 @@ export type SitePreferencesGroupByArgs<ExtArgs extends runtime.Types.Extensions.
take?: number
skip?: number
_count?: SitePreferencesCountAggregateInputType | true
_avg?: SitePreferencesAvgAggregateInputType
_sum?: SitePreferencesSumAggregateInputType
_min?: SitePreferencesMinAggregateInputType
_max?: SitePreferencesMaxAggregateInputType
}
@@ -165,9 +213,13 @@ export type SitePreferencesGroupByOutputType = {
eventsBackground: string | null
leaderboardBackground: string | null
challengesBackground: string | null
eventRegistrationPoints: number
eventFeedbackPoints: number
createdAt: Date
updatedAt: Date
_count: SitePreferencesCountAggregateOutputType | null
_avg: SitePreferencesAvgAggregateOutputType | null
_sum: SitePreferencesSumAggregateOutputType | null
_min: SitePreferencesMinAggregateOutputType | null
_max: SitePreferencesMaxAggregateOutputType | null
}
@@ -196,6 +248,8 @@ export type SitePreferencesWhereInput = {
eventsBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
leaderboardBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
challengesBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
eventRegistrationPoints?: Prisma.IntFilter<"SitePreferences"> | number
eventFeedbackPoints?: Prisma.IntFilter<"SitePreferences"> | number
createdAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string
}
@@ -206,6 +260,8 @@ export type SitePreferencesOrderByWithRelationInput = {
eventsBackground?: Prisma.SortOrderInput | Prisma.SortOrder
leaderboardBackground?: Prisma.SortOrderInput | Prisma.SortOrder
challengesBackground?: Prisma.SortOrderInput | Prisma.SortOrder
eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -219,6 +275,8 @@ export type SitePreferencesWhereUniqueInput = Prisma.AtLeast<{
eventsBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
leaderboardBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
challengesBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
eventRegistrationPoints?: Prisma.IntFilter<"SitePreferences"> | number
eventFeedbackPoints?: Prisma.IntFilter<"SitePreferences"> | number
createdAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string
}, "id">
@@ -229,11 +287,15 @@ export type SitePreferencesOrderByWithAggregationInput = {
eventsBackground?: Prisma.SortOrderInput | Prisma.SortOrder
leaderboardBackground?: Prisma.SortOrderInput | Prisma.SortOrder
challengesBackground?: Prisma.SortOrderInput | Prisma.SortOrder
eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
_count?: Prisma.SitePreferencesCountOrderByAggregateInput
_avg?: Prisma.SitePreferencesAvgOrderByAggregateInput
_max?: Prisma.SitePreferencesMaxOrderByAggregateInput
_min?: Prisma.SitePreferencesMinOrderByAggregateInput
_sum?: Prisma.SitePreferencesSumOrderByAggregateInput
}
export type SitePreferencesScalarWhereWithAggregatesInput = {
@@ -245,6 +307,8 @@ export type SitePreferencesScalarWhereWithAggregatesInput = {
eventsBackground?: Prisma.StringNullableWithAggregatesFilter<"SitePreferences"> | string | null
leaderboardBackground?: Prisma.StringNullableWithAggregatesFilter<"SitePreferences"> | string | null
challengesBackground?: Prisma.StringNullableWithAggregatesFilter<"SitePreferences"> | string | null
eventRegistrationPoints?: Prisma.IntWithAggregatesFilter<"SitePreferences"> | number
eventFeedbackPoints?: Prisma.IntWithAggregatesFilter<"SitePreferences"> | number
createdAt?: Prisma.DateTimeWithAggregatesFilter<"SitePreferences"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"SitePreferences"> | Date | string
}
@@ -255,6 +319,8 @@ export type SitePreferencesCreateInput = {
eventsBackground?: string | null
leaderboardBackground?: string | null
challengesBackground?: string | null
eventRegistrationPoints?: number
eventFeedbackPoints?: number
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -265,6 +331,8 @@ export type SitePreferencesUncheckedCreateInput = {
eventsBackground?: string | null
leaderboardBackground?: string | null
challengesBackground?: string | null
eventRegistrationPoints?: number
eventFeedbackPoints?: number
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -275,6 +343,8 @@ export type SitePreferencesUpdateInput = {
eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number
eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -285,6 +355,8 @@ export type SitePreferencesUncheckedUpdateInput = {
eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number
eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -295,6 +367,8 @@ export type SitePreferencesCreateManyInput = {
eventsBackground?: string | null
leaderboardBackground?: string | null
challengesBackground?: string | null
eventRegistrationPoints?: number
eventFeedbackPoints?: number
createdAt?: Date | string
updatedAt?: Date | string
}
@@ -305,6 +379,8 @@ export type SitePreferencesUpdateManyMutationInput = {
eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number
eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -315,6 +391,8 @@ export type SitePreferencesUncheckedUpdateManyInput = {
eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number
eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
}
@@ -325,16 +403,25 @@ export type SitePreferencesCountOrderByAggregateInput = {
eventsBackground?: Prisma.SortOrder
leaderboardBackground?: Prisma.SortOrder
challengesBackground?: Prisma.SortOrder
eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
export type SitePreferencesAvgOrderByAggregateInput = {
eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder
}
export type SitePreferencesMaxOrderByAggregateInput = {
id?: Prisma.SortOrder
homeBackground?: Prisma.SortOrder
eventsBackground?: Prisma.SortOrder
leaderboardBackground?: Prisma.SortOrder
challengesBackground?: Prisma.SortOrder
eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
@@ -345,10 +432,17 @@ export type SitePreferencesMinOrderByAggregateInput = {
eventsBackground?: Prisma.SortOrder
leaderboardBackground?: Prisma.SortOrder
challengesBackground?: Prisma.SortOrder
eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder
}
export type SitePreferencesSumOrderByAggregateInput = {
eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder
}
export type SitePreferencesSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
@@ -357,6 +451,8 @@ export type SitePreferencesSelect<ExtArgs extends runtime.Types.Extensions.Inter
eventsBackground?: boolean
leaderboardBackground?: boolean
challengesBackground?: boolean
eventRegistrationPoints?: boolean
eventFeedbackPoints?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["sitePreferences"]>
@@ -367,6 +463,8 @@ export type SitePreferencesSelectCreateManyAndReturn<ExtArgs extends runtime.Typ
eventsBackground?: boolean
leaderboardBackground?: boolean
challengesBackground?: boolean
eventRegistrationPoints?: boolean
eventFeedbackPoints?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["sitePreferences"]>
@@ -377,6 +475,8 @@ export type SitePreferencesSelectUpdateManyAndReturn<ExtArgs extends runtime.Typ
eventsBackground?: boolean
leaderboardBackground?: boolean
challengesBackground?: boolean
eventRegistrationPoints?: boolean
eventFeedbackPoints?: boolean
createdAt?: boolean
updatedAt?: boolean
}, ExtArgs["result"]["sitePreferences"]>
@@ -387,11 +487,13 @@ export type SitePreferencesSelectScalar = {
eventsBackground?: boolean
leaderboardBackground?: boolean
challengesBackground?: boolean
eventRegistrationPoints?: boolean
eventFeedbackPoints?: boolean
createdAt?: boolean
updatedAt?: boolean
}
export type SitePreferencesOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "homeBackground" | "eventsBackground" | "leaderboardBackground" | "challengesBackground" | "createdAt" | "updatedAt", ExtArgs["result"]["sitePreferences"]>
export type SitePreferencesOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "homeBackground" | "eventsBackground" | "leaderboardBackground" | "challengesBackground" | "eventRegistrationPoints" | "eventFeedbackPoints" | "createdAt" | "updatedAt", ExtArgs["result"]["sitePreferences"]>
export type $SitePreferencesPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "SitePreferences"
@@ -402,6 +504,8 @@ export type $SitePreferencesPayload<ExtArgs extends runtime.Types.Extensions.Int
eventsBackground: string | null
leaderboardBackground: string | null
challengesBackground: string | null
eventRegistrationPoints: number
eventFeedbackPoints: number
createdAt: Date
updatedAt: Date
}, ExtArgs["result"]["sitePreferences"]>
@@ -832,6 +936,8 @@ export interface SitePreferencesFieldRefs {
readonly eventsBackground: Prisma.FieldRef<"SitePreferences", 'String'>
readonly leaderboardBackground: Prisma.FieldRef<"SitePreferences", 'String'>
readonly challengesBackground: Prisma.FieldRef<"SitePreferences", 'String'>
readonly eventRegistrationPoints: Prisma.FieldRef<"SitePreferences", 'Int'>
readonly eventFeedbackPoints: Prisma.FieldRef<"SitePreferences", 'Int'>
readonly createdAt: Prisma.FieldRef<"SitePreferences", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"SitePreferences", 'DateTime'>
}

View File

@@ -0,0 +1,18 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_SitePreferences" (
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'global',
"homeBackground" TEXT,
"eventsBackground" TEXT,
"leaderboardBackground" TEXT,
"challengesBackground" TEXT,
"eventRegistrationPoints" INTEGER NOT NULL DEFAULT 100,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_SitePreferences" ("challengesBackground", "createdAt", "eventsBackground", "homeBackground", "id", "leaderboardBackground", "updatedAt") SELECT "challengesBackground", "createdAt", "eventsBackground", "homeBackground", "id", "leaderboardBackground", "updatedAt" FROM "SitePreferences";
DROP TABLE "SitePreferences";
ALTER TABLE "new_SitePreferences" RENAME TO "SitePreferences";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,19 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_SitePreferences" (
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'global',
"homeBackground" TEXT,
"eventsBackground" TEXT,
"leaderboardBackground" TEXT,
"challengesBackground" TEXT,
"eventRegistrationPoints" INTEGER NOT NULL DEFAULT 100,
"eventFeedbackPoints" INTEGER NOT NULL DEFAULT 50,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_SitePreferences" ("challengesBackground", "createdAt", "eventRegistrationPoints", "eventsBackground", "homeBackground", "id", "leaderboardBackground", "updatedAt") SELECT "challengesBackground", "createdAt", "eventRegistrationPoints", "eventsBackground", "homeBackground", "id", "leaderboardBackground", "updatedAt" FROM "SitePreferences";
DROP TABLE "SitePreferences";
ALTER TABLE "new_SitePreferences" RENAME TO "SitePreferences";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,19 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_SitePreferences" (
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'global',
"homeBackground" TEXT,
"eventsBackground" TEXT,
"leaderboardBackground" TEXT,
"challengesBackground" TEXT,
"eventRegistrationPoints" INTEGER NOT NULL DEFAULT 100,
"eventFeedbackPoints" INTEGER NOT NULL DEFAULT 100,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_SitePreferences" ("challengesBackground", "createdAt", "eventFeedbackPoints", "eventRegistrationPoints", "eventsBackground", "homeBackground", "id", "leaderboardBackground", "updatedAt") SELECT "challengesBackground", "createdAt", "eventFeedbackPoints", "eventRegistrationPoints", "eventsBackground", "homeBackground", "id", "leaderboardBackground", "updatedAt" FROM "SitePreferences";
DROP TABLE "SitePreferences";
ALTER TABLE "new_SitePreferences" RENAME TO "SitePreferences";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,24 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_EventFeedback" (
"id" TEXT NOT NULL PRIMARY KEY,
"userId" TEXT NOT NULL,
"eventId" TEXT NOT NULL,
"rating" INTEGER NOT NULL,
"comment" TEXT,
"isRead" BOOLEAN NOT NULL DEFAULT false,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
CONSTRAINT "EventFeedback_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "EventFeedback_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
INSERT INTO "new_EventFeedback" ("comment", "createdAt", "eventId", "id", "rating", "updatedAt", "userId") SELECT "comment", "createdAt", "eventId", "id", "rating", "updatedAt", "userId" FROM "EventFeedback";
DROP TABLE "EventFeedback";
ALTER TABLE "new_EventFeedback" RENAME TO "EventFeedback";
CREATE INDEX "EventFeedback_userId_idx" ON "EventFeedback"("userId");
CREATE INDEX "EventFeedback_eventId_idx" ON "EventFeedback"("eventId");
CREATE INDEX "EventFeedback_isRead_idx" ON "EventFeedback"("isRead");
CREATE UNIQUE INDEX "EventFeedback_userId_eventId_key" ON "EventFeedback"("userId", "eventId");
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -84,6 +84,7 @@ model EventFeedback {
eventId String
rating Int
comment String?
isRead Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
event Event @relation(fields: [eventId], references: [id], onDelete: Cascade)
@@ -92,6 +93,7 @@ model EventFeedback {
@@unique([userId, eventId])
@@index([userId])
@@index([eventId])
@@index([isRead])
}
model SitePreferences {
@@ -100,6 +102,8 @@ model SitePreferences {
eventsBackground String?
leaderboardBackground String?
challengesBackground String?
eventRegistrationPoints Int @default(100)
eventFeedbackPoints Int @default(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

View File

@@ -2,6 +2,7 @@ import { prisma } from "../database";
import type { EventFeedback, Prisma } from "@/prisma/generated/prisma/client";
import { ValidationError, NotFoundError } from "../errors";
import { eventService } from "./event.service";
import { sitePreferencesService } from "../preferences/site-preferences.service";
export interface CreateOrUpdateFeedbackInput {
rating: number;
@@ -97,6 +98,8 @@ export class EventFeedbackService {
id: true,
username: true,
email: true,
avatar: true,
score: true,
},
},
},
@@ -168,11 +171,34 @@ export class EventFeedbackService {
throw new NotFoundError("Événement");
}
// Créer ou mettre à jour le feedback
return this.createOrUpdateFeedback(userId, eventId, {
// Vérifier si c'est un nouveau feedback ou une mise à jour
const existingFeedback = await this.getUserFeedback(userId, eventId);
const isNewFeedback = !existingFeedback;
// Récupérer les points à attribuer depuis les préférences du site
const sitePreferences = await sitePreferencesService.getOrCreateSitePreferences();
const pointsToAward = sitePreferences.eventFeedbackPoints || 100;
// Créer ou mettre à jour le feedback et attribuer les points (seulement pour nouveau feedback)
const [feedback] = await Promise.all([
this.createOrUpdateFeedback(userId, eventId, {
rating: data.rating,
comment: data.comment || null,
});
}),
// Attribuer les points seulement si c'est un nouveau feedback
isNewFeedback
? prisma.user.update({
where: { id: userId },
data: {
score: {
increment: pointsToAward,
},
},
})
: Promise.resolve(null),
]);
return feedback;
}
}

View File

@@ -3,6 +3,7 @@ import type { EventRegistration } from "@/prisma/generated/prisma/client";
import { ValidationError, NotFoundError, ConflictError } from "../errors";
import { eventService } from "./event.service";
import { calculateEventStatus } from "@/lib/eventStatus";
import { sitePreferencesService } from "../preferences/site-preferences.service";
/**
* Service de gestion des inscriptions aux événements
@@ -24,18 +25,40 @@ export class EventRegistrationService {
}
/**
* Désinscrit un utilisateur d'un événement
* Désinscrit un utilisateur d'un événement et retire les points attribués
*/
async unregisterUserFromEvent(
userId: string,
eventId: string
): Promise<void> {
await prisma.eventRegistration.deleteMany({
// Vérifier que l'utilisateur est bien inscrit avant de retirer les points
const isRegistered = await this.checkUserRegistration(userId, eventId);
if (!isRegistered) {
return; // Pas d'inscription, rien à faire
}
// Récupérer les points à retirer depuis les préférences du site
const sitePreferences =
await sitePreferencesService.getOrCreateSitePreferences();
const pointsToRemove = sitePreferences.eventRegistrationPoints || 100;
// Supprimer l'inscription et retirer les points en parallèle
await Promise.all([
prisma.eventRegistration.deleteMany({
where: {
userId,
eventId,
},
});
}),
prisma.user.update({
where: { id: userId },
data: {
score: {
decrement: pointsToRemove,
},
},
}),
]);
}
/**
@@ -96,6 +119,35 @@ export class EventRegistrationService {
return count;
}
/**
* Récupère tous les inscrits d'un événement avec leurs informations
*/
async getEventRegistrations(eventId: string) {
return prisma.eventRegistration.findMany({
where: {
eventId,
},
include: {
user: {
select: {
id: true,
username: true,
avatar: true,
score: true,
level: true,
hp: true,
maxHp: true,
xp: true,
maxXp: true,
},
},
},
orderBy: {
createdAt: "asc",
},
});
}
/**
* Valide et inscrit un utilisateur à un événement avec toutes les règles métier
*/
@@ -123,8 +175,25 @@ export class EventRegistrationService {
throw new ConflictError("Vous êtes déjà inscrit à cet événement");
}
// Créer l'inscription
return this.registerUserToEvent(userId, eventId);
// Récupérer les points à attribuer depuis les préférences du site
const sitePreferences =
await sitePreferencesService.getOrCreateSitePreferences();
const pointsToAward = sitePreferences.eventRegistrationPoints || 100;
// Créer l'inscription et attribuer les points en parallèle
const [registration] = await Promise.all([
this.registerUserToEvent(userId, eventId),
prisma.user.update({
where: { id: userId },
data: {
score: {
increment: pointsToAward,
},
},
}),
]);
return registration;
}
}

View File

@@ -7,6 +7,8 @@ export interface UpdateSitePreferencesInput {
eventsBackground?: string | null;
leaderboardBackground?: string | null;
challengesBackground?: string | null;
eventRegistrationPoints?: number;
eventFeedbackPoints?: number;
}
/**
@@ -38,6 +40,8 @@ export class SitePreferencesService {
eventsBackground: null,
leaderboardBackground: null,
challengesBackground: null,
eventRegistrationPoints: 100,
eventFeedbackPoints: 100,
},
});
}
@@ -70,6 +74,14 @@ export class SitePreferencesService {
data.challengesBackground === ""
? null
: (data.challengesBackground ?? undefined),
eventRegistrationPoints:
data.eventRegistrationPoints !== undefined
? data.eventRegistrationPoints
: undefined,
eventFeedbackPoints:
data.eventFeedbackPoints !== undefined
? data.eventFeedbackPoints
: undefined,
},
create: {
id: "global",
@@ -85,6 +97,8 @@ export class SitePreferencesService {
data.challengesBackground === ""
? null
: (data.challengesBackground ?? null),
eventRegistrationPoints: data.eventRegistrationPoints ?? 100,
eventFeedbackPoints: data.eventFeedbackPoints ?? 100,
},
});
}