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 */}