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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
Reference in New Issue
Block a user