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.

This commit is contained in:
Julien Froidefond
2025-12-16 16:38:01 +01:00
parent f45cc1839e
commit 3dd82c2bd4
20 changed files with 652 additions and 19 deletions

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

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

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

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

@@ -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";
@@ -42,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) {
@@ -92,6 +117,18 @@ export default function PlayerStats({ initialUserData }: PlayerStatsProps) {
}
}, [session, initialUserData]);
// Écouter les événements de refresh du score
useEffect(() => {
const handleRefreshScore = () => {
refreshUserData();
};
window.addEventListener("refreshUserScore", handleRefreshScore);
return () => {
window.removeEventListener("refreshUserScore", handleRefreshScore);
};
}, [refreshUserData]);
const { username, avatar, level, score } = userData;
return (
@@ -136,7 +173,6 @@ export default function PlayerStats({ initialUserData }: PlayerStatsProps) {
</div>
</div>
</div>
</div>
);
}