From b1f36f6210a74550f97244647352526abfa4ebc6 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Tue, 9 Dec 2025 14:02:27 +0100 Subject: [PATCH] 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. --- app/admin/page.tsx | 72 +++++++- app/api/profile/password/route.ts | 80 +++++++++ app/profile/page.tsx | 134 +++++++++++++++ components/HeroSection.tsx | 271 +++++++++++++++++++++++++++++- components/ImageSelector.tsx | 131 ++++++++------- 5 files changed, 620 insertions(+), 68 deletions(-) create mode 100644 app/api/profile/password/route.ts diff --git a/app/admin/page.tsx b/app/admin/page.tsx index bc10569..2409d22 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -230,16 +230,72 @@ export default function AdminPage() { ) : ( -
-
- Home: {preferences?.homeBackground || "Par défaut"} +
+
+ + Home: + + {preferences?.homeBackground ? ( +
+ Home background { + e.currentTarget.src = "/got-2.jpg"; + }} + /> + + {preferences.homeBackground} + +
+ ) : ( + Par défaut + )}
-
- Events: {preferences?.eventsBackground || "Par défaut"} +
+ + Events: + + {preferences?.eventsBackground ? ( +
+ Events background { + e.currentTarget.src = "/got-2.jpg"; + }} + /> + + {preferences.eventsBackground} + +
+ ) : ( + Par défaut + )}
-
- Leaderboard:{" "} - {preferences?.leaderboardBackground || "Par défaut"} +
+ + Leaderboard: + + {preferences?.leaderboardBackground ? ( +
+ Leaderboard background { + e.currentTarget.src = "/got-2.jpg"; + }} + /> + + {preferences.leaderboardBackground} + +
+ ) : ( + Par défaut + )}
)} diff --git a/app/api/profile/password/route.ts b/app/api/profile/password/route.ts new file mode 100644 index 0000000..a6cf37a --- /dev/null +++ b/app/api/profile/password/route.ts @@ -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 } + ); + } +} + diff --git a/app/profile/page.tsx b/app/profile/page.tsx index 7a12a54..1acbe1f 100644 --- a/app/profile/page.tsx +++ b/app/profile/page.tsx @@ -38,6 +38,13 @@ export default function ProfilePage() { const [avatar, setAvatar] = useState(null); const fileInputRef = useRef(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 (
@@ -356,6 +401,95 @@ export default function ProfilePage() {
+ + {/* Password Change Section - Separate form */} +
+
+

+ Mot de passe +

+ {!showPasswordForm && ( + + )} +
+ + {showPasswordForm && ( +
+
+ + 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 + /> +
+ +
+ + 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} + /> +

+ Minimum 6 caractères +

+
+ +
+ + 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} + /> +
+ +
+ + +
+
+ )} +
diff --git a/components/HeroSection.tsx b/components/HeroSection.tsx index e6bb07b..7247a14 100644 --- a/components/HeroSection.tsx +++ b/components/HeroSection.tsx @@ -2,9 +2,94 @@ import { useBackgroundImage } from "@/hooks/usePreferences"; import Link from "next/link"; +import { useState, useEffect } from "react"; + +interface Particle { + width: number; + height: number; + left: number; + top: number; + duration: number; + delay: number; + shadow: number; + fadeIn: number; + fadeOut: number; + visibleDuration: number; + moveY1: number; + moveX1: number; + moveY2: number; + moveX2: number; + moveY3: number; + moveX3: number; + moveY4: number; + moveX4: number; + moveY5: number; + moveX5: number; + moveY6: number; + moveX6: number; +} + +interface Orb { + width: number; + height: number; + left: number; + top: number; + duration: number; + delay: number; +} export default function HeroSection() { const backgroundImage = useBackgroundImage("home", "/got-2.jpg"); + const [particles, setParticles] = useState([]); + const [orbs, setOrbs] = useState([]); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + // Generate particles - more visible and dynamic + setParticles( + Array.from({ length: 30 }, () => { + const fadeIn = Math.random() * 5 + 2; // 2-7% of animation - faster fade in + const visibleDuration = Math.random() * 30 + 20; // 20-50% of animation + const fadeOut = Math.random() * 5 + 2; // 2-7% of animation - faster fade out + return { + width: Math.random() * 6 + 3, + height: Math.random() * 6 + 3, + left: Math.random() * 100, + top: Math.random() * 100, + duration: 10 + Math.random() * 15, + delay: Math.random() * 8, + shadow: Math.random() * 15 + 8, + fadeIn: fadeIn, + fadeOut: fadeOut, + visibleDuration: visibleDuration, + moveY1: 20 + Math.random() * 20, + moveX1: Math.random() * 10 - 5, + moveY2: 40 + Math.random() * 20, + moveX2: Math.random() * 15 - 7, + moveY3: 60 + Math.random() * 20, + moveX3: Math.random() * 10 - 5, + moveY4: 80 + Math.random() * 20, + moveX4: Math.random() * 10 - 5, + moveY5: 100 + Math.random() * 20, + moveX5: Math.random() * 10 - 5, + moveY6: 120 + Math.random() * 20, + moveX6: Math.random() * 10 - 5, + }; + }) + ); + // Generate orbs + setOrbs( + Array.from({ length: 4 }, () => ({ + width: 100 + Math.random() * 200, + height: 100 + Math.random() * 200, + left: Math.random() * 80, + top: Math.random() * 80, + duration: 20 + Math.random() * 15, + delay: Math.random() * 10, + })) + ); + }, []); return (
@@ -16,7 +101,89 @@ export default function HeroSection() { }} > {/* Dark overlay for readability */} -
+
+ + {/* Animated particles */} + {mounted && ( +
+ {particles.map((particle, i) => ( +
+ ))} +
+ )} + + {/* Animated light rays */} +
+ {[...Array(3)].map((_, i) => { + const rotation = -15 + i * 15; + return ( +
+ ); + })} +
+ + {/* Glowing orbs */} + {mounted && ( +
+ {orbs.map((orb, i) => ( +
+ ))} +
+ )} + + {/* Shimmer effect */} +
+
+
{/* Hero Content */} @@ -77,6 +244,108 @@ export default function HeroSection() { transform: translateY(-20px); } } + + ${particles + .map((particle, i) => { + const fadeInPercent = particle.fadeIn; + const visibleStart = fadeInPercent; + const visibleEnd = visibleStart + particle.visibleDuration; + const fadeOutStart = visibleEnd; + const fadeOutEnd = Math.min(100, fadeOutStart + particle.fadeOut); + + return ` + @keyframes float-particle-${i} { + 0% { + transform: translateY(0px) translateX(0px) scale(0.8); + opacity: 0; + } + ${fadeInPercent}% { + transform: translateY(-${particle.moveY1}px) translateX(${particle.moveX1}px) scale(1); + opacity: 0.9; + } + ${visibleStart}% { + transform: translateY(-${particle.moveY2}px) translateX(${particle.moveX2}px) scale(1.1); + opacity: 1; + } + ${visibleEnd}% { + transform: translateY(-${particle.moveY3}px) translateX(${particle.moveX3}px) scale(1.05); + opacity: 1; + } + ${fadeOutStart}% { + transform: translateY(-${particle.moveY4}px) translateX(${particle.moveX4}px) scale(0.9); + opacity: 0.7; + } + ${fadeOutEnd}% { + transform: translateY(-${particle.moveY5}px) translateX(${particle.moveX5}px) scale(0.5); + opacity: 0; + } + 100% { + transform: translateY(-${particle.moveY6}px) translateX(${particle.moveX6}px) scale(0.3); + opacity: 0; + } + } + `; + }) + .join("")} + + @keyframes light-ray-0 { + 0%, + 100% { + opacity: 0.2; + transform: rotate(-15deg) scaleY(0.8); + } + 50% { + opacity: 0.5; + transform: rotate(-15deg) scaleY(1.2); + } + } + @keyframes light-ray-1 { + 0%, + 100% { + opacity: 0.2; + transform: rotate(0deg) scaleY(0.8); + } + 50% { + opacity: 0.5; + transform: rotate(0deg) scaleY(1.2); + } + } + @keyframes light-ray-2 { + 0%, + 100% { + opacity: 0.2; + transform: rotate(15deg) scaleY(0.8); + } + 50% { + opacity: 0.5; + transform: rotate(15deg) scaleY(1.2); + } + } + + @keyframes orb-float { + 0%, + 100% { + transform: translate(0, 0) scale(1); + opacity: 0.2; + } + 33% { + transform: translate(30px, -30px) scale(1.1); + opacity: 0.3; + } + 66% { + transform: translate(-20px, 20px) scale(0.9); + opacity: 0.25; + } + } + + @keyframes shimmer { + 0% { + transform: translateX(-100%) skewX(-20deg); + } + 100% { + transform: translateX(200%) skewX(-20deg); + } + } `}
); diff --git a/components/ImageSelector.tsx b/components/ImageSelector.tsx index 3b2f5cb..c738c65 100644 --- a/components/ImageSelector.tsx +++ b/components/ImageSelector.tsx @@ -79,71 +79,84 @@ export default function ImageSelector({
- {/* Prévisualisation */} - {value && ( -
-
- Preview { - e.currentTarget.src = "/got-2.jpg"; // Image par défaut en cas d'erreur - }} +
+ {/* Colonne gauche - Image */} +
+ {value ? ( +
+ Preview { + e.currentTarget.src = "/got-2.jpg"; // Image par défaut en cas d'erreur + }} + /> + +
+ ) : ( +
+ Aucune +
+ )} +
+ + {/* Colonne droite - Contrôles */} +
+ {/* Input URL */} +
+ setUrlInput(e.target.value)} + onKeyPress={(e) => e.key === "Enter" && handleUrlSubmit()} + placeholder="https://example.com/image.jpg ou /image.jpg" + className="flex-1 px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm" />
-

{value}

+ + {/* Upload depuis le disque */} +
+ + + +
+ + {/* Chemin de l'image */} + {value && ( +

{value}

+ )}
- )} - - {/* Input URL */} -
- setUrlInput(e.target.value)} - onKeyPress={(e) => e.key === "Enter" && handleUrlSubmit()} - placeholder="https://example.com/image.jpg ou /image.jpg" - className="flex-1 px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm" - /> - -
- - {/* Upload depuis le disque */} -
- - -
{/* Galerie d'images */}