Enhance Admin and Profile UI: Update admin page to display user preferences with improved layout and visuals. Add password change functionality to profile page, including form handling and validation. Refactor ImageSelector for better image preview and upload experience.

This commit is contained in:
Julien Froidefond
2025-12-09 14:02:27 +01:00
parent 4e38bd1e8e
commit b1f36f6210
5 changed files with 620 additions and 68 deletions

View File

@@ -230,16 +230,72 @@ export default function AdminPage() {
</div>
</div>
) : (
<div className="space-y-2 text-sm text-gray-400">
<div>
Home: {preferences?.homeBackground || "Par défaut"}
<div className="space-y-4">
<div className="flex items-center gap-4">
<span className="text-pixel-gold font-bold min-w-[120px]">
Home:
</span>
{preferences?.homeBackground ? (
<div className="flex items-center gap-3">
<img
src={preferences.homeBackground}
alt="Home background"
className="w-20 h-12 object-cover rounded border border-pixel-gold/30"
onError={(e) => {
e.currentTarget.src = "/got-2.jpg";
}}
/>
<span className="text-xs text-gray-400 truncate max-w-xs">
{preferences.homeBackground}
</span>
</div>
) : (
<span className="text-gray-400">Par défaut</span>
)}
</div>
<div>
Events: {preferences?.eventsBackground || "Par défaut"}
<div className="flex items-center gap-4">
<span className="text-pixel-gold font-bold min-w-[120px]">
Events:
</span>
{preferences?.eventsBackground ? (
<div className="flex items-center gap-3">
<img
src={preferences.eventsBackground}
alt="Events background"
className="w-20 h-12 object-cover rounded border border-pixel-gold/30"
onError={(e) => {
e.currentTarget.src = "/got-2.jpg";
}}
/>
<span className="text-xs text-gray-400 truncate max-w-xs">
{preferences.eventsBackground}
</span>
</div>
) : (
<span className="text-gray-400">Par défaut</span>
)}
</div>
<div>
Leaderboard:{" "}
{preferences?.leaderboardBackground || "Par défaut"}
<div className="flex items-center gap-4">
<span className="text-pixel-gold font-bold min-w-[120px]">
Leaderboard:
</span>
{preferences?.leaderboardBackground ? (
<div className="flex items-center gap-3">
<img
src={preferences.leaderboardBackground}
alt="Leaderboard background"
className="w-20 h-12 object-cover rounded border border-pixel-gold/30"
onError={(e) => {
e.currentTarget.src = "/got-2.jpg";
}}
/>
<span className="text-xs text-gray-400 truncate max-w-xs">
{preferences.leaderboardBackground}
</span>
</div>
) : (
<span className="text-gray-400">Par défaut</span>
)}
</div>
</div>
)}

View File

@@ -0,0 +1,80 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import bcrypt from "bcryptjs";
export async function PUT(request: Request) {
try {
const session = await auth();
if (!session?.user) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
const body = await request.json();
const { currentPassword, newPassword, confirmPassword } = body;
// Validation
if (!currentPassword || !newPassword || !confirmPassword) {
return NextResponse.json(
{ error: "Tous les champs sont requis" },
{ status: 400 }
);
}
if (newPassword.length < 6) {
return NextResponse.json(
{ error: "Le nouveau mot de passe doit contenir au moins 6 caractères" },
{ status: 400 }
);
}
if (newPassword !== confirmPassword) {
return NextResponse.json(
{ error: "Les mots de passe ne correspondent pas" },
{ status: 400 }
);
}
// Récupérer l'utilisateur avec le mot de passe
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { password: true },
});
if (!user) {
return NextResponse.json(
{ error: "Utilisateur non trouvé" },
{ status: 404 }
);
}
// Vérifier l'ancien mot de passe
const isPasswordValid = await bcrypt.compare(currentPassword, user.password);
if (!isPasswordValid) {
return NextResponse.json(
{ error: "Mot de passe actuel incorrect" },
{ status: 400 }
);
}
// Hasher le nouveau mot de passe
const hashedPassword = await bcrypt.hash(newPassword, 10);
// Mettre à jour le mot de passe
await prisma.user.update({
where: { id: session.user.id },
data: { password: hashedPassword },
});
return NextResponse.json({ message: "Mot de passe modifié avec succès" });
} catch (error) {
console.error("Error updating password:", error);
return NextResponse.json(
{ error: "Erreur lors de la modification du mot de passe" },
{ status: 500 }
);
}
}

View File

@@ -38,6 +38,13 @@ export default function ProfilePage() {
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") {
@@ -140,6 +147,44 @@ export default function ProfilePage() {
}
};
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">
@@ -356,6 +401,95 @@ export default function ProfilePage() {
</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>