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

@@ -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<Particle[]>([]);
const [orbs, setOrbs] = useState<Orb[]>([]);
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 (
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
@@ -16,7 +101,89 @@ export default function HeroSection() {
}}
>
{/* Dark overlay for readability */}
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80 z-[1]"></div>
{/* Animated particles */}
{mounted && (
<div className="absolute inset-0 overflow-hidden z-[2]">
{particles.map((particle, i) => (
<div
key={i}
className="absolute rounded-full"
style={{
width: `${particle.width}px`,
height: `${particle.height}px`,
left: `${particle.left}%`,
top: `${particle.top}%`,
background: `radial-gradient(circle, rgba(218, 165, 32, 0.9) 0%, rgba(218, 165, 32, 0.4) 50%, transparent 100%)`,
animation: `float-particle-${i} ${particle.duration}s infinite ease-in-out`,
animationDelay: `${particle.delay}s`,
boxShadow: `0 0 ${
particle.shadow
}px rgba(218, 165, 32, 0.8), 0 0 ${
particle.shadow * 2
}px rgba(218, 165, 32, 0.4)`,
filter: "blur(0.5px)",
}}
/>
))}
</div>
)}
{/* Animated light rays */}
<div className="absolute inset-0 overflow-hidden opacity-30">
{[...Array(3)].map((_, i) => {
const rotation = -15 + i * 15;
return (
<div
key={i}
className="absolute w-1 bg-gradient-to-b from-transparent via-pixel-gold/20 to-transparent"
style={{
height: "100%",
left: `${20 + i * 30}%`,
transform: `rotate(${rotation}deg)`,
animation: `light-ray-${i} ${
8 + i * 2
}s infinite ease-in-out`,
animationDelay: `${i * 2}s`,
transformOrigin: "top center",
}}
/>
);
})}
</div>
{/* Glowing orbs */}
{mounted && (
<div className="absolute inset-0 overflow-hidden">
{orbs.map((orb, i) => (
<div
key={i}
className="absolute rounded-full blur-xl"
style={{
width: `${orb.width}px`,
height: `${orb.height}px`,
left: `${orb.left}%`,
top: `${orb.top}%`,
background: `radial-gradient(circle, rgba(218, 165, 32, 0.2) 0%, transparent 70%)`,
animation: `orb-float ${orb.duration}s infinite ease-in-out`,
animationDelay: `${orb.delay}s`,
}}
/>
))}
</div>
)}
{/* Shimmer effect */}
<div className="absolute inset-0 overflow-hidden">
<div
className="absolute inset-0 bg-gradient-to-r from-transparent via-pixel-gold/10 to-transparent"
style={{
transform: "skewX(-20deg)",
animation: "shimmer 8s infinite",
}}
/>
</div>
</div>
{/* 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);
}
}
`}</style>
</section>
);

View File

@@ -79,71 +79,84 @@ export default function ImageSelector({
<div className="space-y-3">
<label className="block text-sm text-gray-300 mb-1">{label}</label>
{/* Prévisualisation */}
{value && (
<div className="mb-3">
<div className="relative w-full h-48 border border-pixel-gold/30 rounded overflow-hidden bg-black/60">
<img
src={value}
alt="Preview"
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.src = "/got-2.jpg"; // Image par défaut en cas d'erreur
}}
<div className="flex gap-4">
{/* Colonne gauche - Image */}
<div className="flex-shrink-0">
{value ? (
<div className="relative w-48 h-32 border border-pixel-gold/30 rounded overflow-hidden bg-black/60">
<img
src={value}
alt="Preview"
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.src = "/got-2.jpg"; // Image par défaut en cas d'erreur
}}
/>
<button
onClick={() => onChange("")}
className="absolute top-2 right-2 px-2 py-1 bg-red-900/80 text-red-200 text-xs rounded hover:bg-red-900 transition"
>
</button>
</div>
) : (
<div className="w-48 h-32 border border-pixel-gold/30 rounded bg-black/60 flex items-center justify-center">
<span className="text-xs text-gray-500">Aucune</span>
</div>
)}
</div>
{/* Colonne droite - Contrôles */}
<div className="flex-1 space-y-3">
{/* Input URL */}
<div className="flex gap-2">
<input
type="text"
value={urlInput}
onChange={(e) => 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"
/>
<button
onClick={() => onChange("")}
className="absolute top-2 right-2 px-2 py-1 bg-red-900/80 text-red-200 text-xs rounded hover:bg-red-900 transition"
onClick={handleUrlSubmit}
className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition"
>
URL
</button>
</div>
<p className="text-xs text-gray-400 mt-1 truncate">{value}</p>
{/* Upload depuis le disque */}
<div className="flex gap-2">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileUpload}
className="hidden"
id={`file-${label}`}
/>
<label
htmlFor={`file-${label}`}
className={`flex-1 px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition text-center cursor-pointer ${
uploading ? "opacity-50 cursor-not-allowed" : ""
}`}
>
{uploading ? "Upload..." : "Upload depuis le disque"}
</label>
<button
onClick={() => setShowGallery(!showGallery)}
className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition"
>
{showGallery ? "Masquer" : "Galerie"}
</button>
</div>
{/* Chemin de l'image */}
{value && (
<p className="text-xs text-gray-400 truncate">{value}</p>
)}
</div>
)}
{/* Input URL */}
<div className="flex gap-2">
<input
type="text"
value={urlInput}
onChange={(e) => 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"
/>
<button
onClick={handleUrlSubmit}
className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition"
>
URL
</button>
</div>
{/* Upload depuis le disque */}
<div className="flex gap-2">
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleFileUpload}
className="hidden"
id={`file-${label}`}
/>
<label
htmlFor={`file-${label}`}
className={`flex-1 px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition text-center cursor-pointer ${
uploading ? "opacity-50 cursor-not-allowed" : ""
}`}
>
{uploading ? "Upload..." : "Upload depuis le disque"}
</label>
<button
onClick={() => setShowGallery(!showGallery)}
className="px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition"
>
{showGallery ? "Masquer" : "Galerie"}
</button>
</div>
{/* Galerie d'images */}