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:
@@ -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>
|
||||
<div>
|
||||
Events: {preferences?.eventsBackground || "Par défaut"}
|
||||
) : (
|
||||
<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]">
|
||||
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 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>
|
||||
)}
|
||||
|
||||
80
app/api/profile/password/route.ts
Normal file
80
app/api/profile/password/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,13 @@ export default function ProfilePage() {
|
||||
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") {
|
||||
router.push("/login");
|
||||
@@ -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>
|
||||
|
||||
@@ -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,10 +79,11 @@ 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">
|
||||
<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"
|
||||
@@ -98,10 +99,15 @@ export default function ImageSelector({
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1 truncate">{value}</p>
|
||||
) : (
|
||||
<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
|
||||
@@ -146,6 +152,13 @@ export default function ImageSelector({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Chemin de l'image */}
|
||||
{value && (
|
||||
<p className="text-xs text-gray-400 truncate">{value}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Galerie d'images */}
|
||||
{showGallery && (
|
||||
<div className="mt-4 p-4 bg-black/40 border border-pixel-gold/20 rounded">
|
||||
|
||||
Reference in New Issue
Block a user