Refactor page components to use NavigationWrapper and integrate Prisma for data fetching. Update EventsSection and LeaderboardSection to accept props for events and leaderboard data, enhancing performance and user experience. Implement user authentication in ProfilePage and AdminPage, ensuring secure access to user data.

This commit is contained in:
Julien Froidefond
2025-12-09 14:11:47 +01:00
parent b1f36f6210
commit 67131f6470
14 changed files with 1041 additions and 944 deletions

View File

@@ -1,498 +1,44 @@
"use client";
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { getBackgroundImage } from "@/lib/preferences";
import NavigationWrapper from "@/components/NavigationWrapper";
import ProfileForm from "@/components/ProfileForm";
import { useEffect, useState, useRef } from "react";
import { useSession } from "next-auth/react";
import { useRouter } from "next/navigation";
import Navigation from "@/components/Navigation";
import { useBackgroundImage } from "@/hooks/usePreferences";
export default async function ProfilePage() {
const session = await auth();
interface UserProfile {
id: string;
email: string;
username: string;
avatar: string | null;
hp: number;
maxHp: number;
xp: number;
maxXp: number;
level: number;
score: number;
createdAt: string;
}
const formatNumber = (num: number): string => {
return num.toLocaleString("en-US");
};
export default function ProfilePage() {
const { data: session, status } = useSession();
const router = useRouter();
const backgroundImage = useBackgroundImage("home", "/got-background.jpg");
const [profile, setProfile] = useState<UserProfile | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const [username, setUsername] = useState("");
const [avatar, setAvatar] = useState<string | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [uploadingAvatar, setUploadingAvatar] = useState(false);
// Password change form state
const [showPasswordForm, setShowPasswordForm] = useState(false);
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [changingPassword, setChangingPassword] = useState(false);
useEffect(() => {
if (status === "unauthenticated") {
router.push("/login");
return;
}
if (status === "authenticated" && session?.user) {
fetchProfile();
}
}, [status, session, router]);
const fetchProfile = async () => {
try {
const response = await fetch("/api/profile");
if (response.ok) {
const data = await response.json();
setProfile(data);
setUsername(data.username);
setAvatar(data.avatar);
} else {
setError("Erreur lors du chargement du profil");
}
} catch (err) {
console.error("Error fetching profile:", err);
setError("Erreur lors du chargement du profil");
} finally {
setLoading(false);
}
};
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploadingAvatar(true);
setError(null);
try {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/profile/avatar", {
method: "POST",
body: formData,
});
if (response.ok) {
const data = await response.json();
setAvatar(data.url);
setSuccess("Avatar mis à jour avec succès");
setTimeout(() => setSuccess(null), 3000);
} else {
const errorData = await response.json();
setError(errorData.error || "Erreur lors de l'upload de l'avatar");
}
} catch (err) {
console.error("Error uploading avatar:", err);
setError("Erreur lors de l'upload de l'avatar");
} finally {
setUploadingAvatar(false);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
setError(null);
setSuccess(null);
try {
const response = await fetch("/api/profile", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
username,
avatar,
}),
});
if (response.ok) {
const data = await response.json();
setProfile(data);
setSuccess("Profil mis à jour avec succès");
setTimeout(() => setSuccess(null), 3000);
} else {
const errorData = await response.json();
setError(errorData.error || "Erreur lors de la mise à jour");
}
} catch (err) {
console.error("Error updating profile:", err);
setError("Erreur lors de la mise à jour du profil");
} finally {
setSaving(false);
}
};
const handlePasswordChange = async (e: React.FormEvent) => {
e.preventDefault();
setChangingPassword(true);
setError(null);
setSuccess(null);
try {
const response = await fetch("/api/profile/password", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
currentPassword,
newPassword,
confirmPassword,
}),
});
if (response.ok) {
setSuccess("Mot de passe modifié avec succès");
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
setShowPasswordForm(false);
setTimeout(() => setSuccess(null), 3000);
} else {
const errorData = await response.json();
setError(errorData.error || "Erreur lors de la modification du mot de passe");
}
} catch (err) {
console.error("Error changing password:", err);
setError("Erreur lors de la modification du mot de passe");
} finally {
setChangingPassword(false);
}
};
if (loading || status === "loading") {
return (
<main className="min-h-screen bg-black relative">
<Navigation />
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
<div className="text-pixel-gold text-xl">Chargement...</div>
</section>
</main>
);
if (!session?.user) {
redirect("/login");
}
if (!profile) {
return (
<main className="min-h-screen bg-black relative">
<Navigation />
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
<div className="text-red-400 text-xl">Erreur lors du chargement du profil</div>
</section>
</main>
);
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: {
id: true,
email: true,
username: true,
avatar: true,
hp: true,
maxHp: true,
xp: true,
maxXp: true,
level: true,
score: true,
createdAt: true,
},
});
if (!user) {
redirect("/login");
}
const hpPercentage = (profile.hp / profile.maxHp) * 100;
const xpPercentage = (profile.xp / profile.maxXp) * 100;
const hpColor =
hpPercentage > 60
? "from-green-600 to-green-700"
: hpPercentage > 30
? "from-yellow-600 to-orange-700"
: "from-red-700 to-red-900";
const backgroundImage = await getBackgroundImage("home", "/got-background.jpg");
return (
<main className="min-h-screen bg-black relative">
<Navigation />
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${backgroundImage}')`,
}}
>
{/* Dark overlay for readability */}
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
</div>
{/* Content */}
<div className="relative z-10 w-full max-w-4xl mx-auto px-8 py-16">
{/* Title Section */}
<div className="text-center mb-12">
<h1 className="text-5xl md:text-7xl font-gaming font-black mb-4 tracking-tight">
<span
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
style={{
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
}}
>
PROFIL
</span>
</h1>
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
<span></span>
<span>Gérez votre profil</span>
<span></span>
</div>
</div>
{/* Profile Card */}
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm">
<form onSubmit={handleSubmit} className="p-8 space-y-8">
{/* Messages */}
{error && (
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
{error}
</div>
)}
{success && (
<div className="bg-green-900/50 border border-green-500/50 text-green-400 px-4 py-3 rounded text-sm">
{success}
</div>
)}
{/* Avatar Section */}
<div className="flex flex-col items-center gap-4">
<div className="relative">
<div className="w-32 h-32 rounded-full border-4 border-pixel-gold/50 overflow-hidden bg-gray-900 flex items-center justify-center">
{avatar ? (
<img
src={avatar}
alt={username}
className="w-full h-full object-cover"
/>
) : (
<span className="text-pixel-gold text-4xl font-bold">
{username.charAt(0).toUpperCase()}
</span>
)}
</div>
{uploadingAvatar && (
<div className="absolute inset-0 bg-black/60 flex items-center justify-center rounded-full">
<div className="text-pixel-gold text-sm">Upload...</div>
</div>
)}
</div>
<div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleAvatarUpload}
className="hidden"
id="avatar-upload"
/>
<label
htmlFor="avatar-upload"
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition cursor-pointer inline-block"
>
{uploadingAvatar ? "Upload en cours..." : "Changer l'avatar"}
</label>
</div>
</div>
{/* Username Field */}
<div>
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
Nom d'utilisateur
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
required
minLength={3}
maxLength={20}
/>
<p className="text-gray-500 text-xs mt-1">
3-20 caractères
</p>
</div>
{/* Stats Display */}
<div className="border-t border-pixel-gold/20 pt-6">
<h3 className="text-pixel-gold text-sm uppercase tracking-widest mb-4">
Statistiques
</h3>
<div className="grid grid-cols-2 gap-4 mb-4">
<div className="bg-black/40 border border-pixel-gold/20 rounded p-4">
<div className="text-gray-400 text-xs uppercase mb-1">Score</div>
<div className="text-pixel-gold text-xl font-bold">
{formatNumber(profile.score)}
</div>
</div>
<div className="bg-black/40 border border-pixel-gold/20 rounded p-4">
<div className="text-gray-400 text-xs uppercase mb-1">Niveau</div>
<div className="text-pixel-gold text-xl font-bold">
Lv.{profile.level}
</div>
</div>
</div>
{/* HP Bar */}
<div className="mb-4">
<div className="flex justify-between text-xs text-gray-400 mb-1">
<span>HP</span>
<span>{profile.hp} / {profile.maxHp}</span>
</div>
<div className="relative h-3 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>
</div>
{/* XP Bar */}
<div>
<div className="flex justify-between text-xs text-gray-400 mb-1">
<span>XP</span>
<span>{formatNumber(profile.xp)} / {formatNumber(profile.maxXp)}</span>
</div>
<div className="relative h-3 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>
</div>
</div>
{/* Email (read-only) */}
<div>
<label className="block text-gray-500 text-sm uppercase tracking-widest mb-2">
Email
</label>
<input
type="email"
value={profile.email}
disabled
className="w-full px-4 py-3 bg-black/20 border border-gray-700/50 rounded text-gray-500 cursor-not-allowed"
/>
</div>
{/* Submit Button */}
<div className="flex justify-end gap-4 pt-4 border-t border-pixel-gold/20">
<button
type="submit"
disabled={saving}
className="px-6 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{saving ? "Enregistrement..." : "Enregistrer les modifications"}
</button>
</div>
</form>
{/* Password Change Section - Separate form */}
<div className="border-t border-pixel-gold/20 p-8">
<div className="flex items-center justify-between mb-4">
<h3 className="text-pixel-gold text-sm uppercase tracking-widest">
Mot de passe
</h3>
{!showPasswordForm && (
<button
type="button"
onClick={() => setShowPasswordForm(true)}
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
>
Changer le mot de passe
</button>
)}
</div>
{showPasswordForm && (
<form onSubmit={handlePasswordChange} className="space-y-4">
<div>
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
Mot de passe actuel
</label>
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
required
/>
</div>
<div>
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
Nouveau mot de passe
</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
required
minLength={6}
/>
<p className="text-gray-500 text-xs mt-1">
Minimum 6 caractères
</p>
</div>
<div>
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
Confirmer le nouveau mot de passe
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
required
minLength={6}
/>
</div>
<div className="flex justify-end gap-4">
<button
type="button"
onClick={() => {
setShowPasswordForm(false);
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
setError(null);
}}
className="px-4 py-2 border border-gray-600/50 bg-black/40 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/40 hover:border-gray-500 transition"
>
Annuler
</button>
<button
type="submit"
disabled={changingPassword}
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
>
{changingPassword ? "Modification..." : "Modifier le mot de passe"}
</button>
</div>
</form>
)}
</div>
</div>
</div>
</section>
<NavigationWrapper />
<ProfileForm initialProfile={user} backgroundImage={backgroundImage} />
</main>
);
}