Refactor admin preferences management to use global site preferences, update UI components for better user experience, and implement image selection for background settings.

This commit is contained in:
Julien Froidefond
2025-12-09 08:37:52 +01:00
parent 4486f305f2
commit 8c326bdd20
21 changed files with 1853 additions and 199 deletions

View File

@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useBackgroundImage } from "@/hooks/usePreferences";
interface Event {
id: string;
@@ -67,6 +68,7 @@ const getStatusBadge = (status: Event["status"]) => {
export default function EventsPageSection() {
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const backgroundImage = useBackgroundImage("events", "/got-2.jpg");
useEffect(() => {
fetch("/api/events")
@@ -94,7 +96,7 @@ export default function EventsPageSection() {
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('/got-2.jpg')`,
backgroundImage: `url('${backgroundImage}')`,
}}
>
{/* Dark overlay for readability */}

View File

@@ -1,13 +1,17 @@
"use client";
import { useBackgroundImage } from "@/hooks/usePreferences";
export default function HeroSection() {
const backgroundImage = useBackgroundImage("home", "/got-2.jpg");
return (
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
{/* Background Image */}
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('/got-2.jpg')`,
backgroundImage: `url('${backgroundImage}')`,
}}
>
{/* Dark overlay for readability */}

View File

@@ -0,0 +1,194 @@
"use client";
import { useState, useEffect, useRef } from "react";
interface ImageSelectorProps {
value: string;
onChange: (url: string) => void;
label: string;
}
export default function ImageSelector({
value,
onChange,
label,
}: ImageSelectorProps) {
const [availableImages, setAvailableImages] = useState<string[]>([]);
const [loading, setLoading] = useState(false);
const [uploading, setUploading] = useState(false);
const [urlInput, setUrlInput] = useState("");
const [showGallery, setShowGallery] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
fetchAvailableImages();
}, []);
const fetchAvailableImages = async () => {
try {
const response = await fetch("/api/admin/images/list");
if (response.ok) {
const data = await response.json();
setAvailableImages(data.images || []);
}
} catch (error) {
console.error("Error fetching images:", error);
}
};
const handleFileUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
setUploading(true);
try {
const formData = new FormData();
formData.append("file", file);
const response = await fetch("/api/admin/images/upload", {
method: "POST",
body: formData,
});
if (response.ok) {
const data = await response.json();
onChange(data.url);
await fetchAvailableImages(); // Rafraîchir la liste
} else {
alert("Erreur lors de l'upload de l'image");
}
} catch (error) {
console.error("Error uploading image:", error);
alert("Erreur lors de l'upload de l'image");
} finally {
setUploading(false);
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
}
};
const handleUrlSubmit = () => {
if (urlInput.trim()) {
onChange(urlInput.trim());
setUrlInput("");
}
};
return (
<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
}}
/>
<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>
<p className="text-xs text-gray-400 mt-1 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 */}
{showGallery && (
<div className="mt-4 p-4 bg-black/40 border border-pixel-gold/20 rounded">
<h4 className="text-sm text-gray-300 mb-3">Images disponibles</h4>
<div className="grid grid-cols-3 md:grid-cols-4 gap-3 max-h-64 overflow-y-auto">
{availableImages.length === 0 ? (
<div className="col-span-full text-center text-gray-400 text-sm py-4">
Aucune image disponible
</div>
) : (
availableImages.map((imageUrl) => (
<button
key={imageUrl}
onClick={() => {
onChange(imageUrl);
setShowGallery(false);
}}
className={`relative aspect-video rounded overflow-hidden border-2 transition ${
value === imageUrl
? "border-pixel-gold"
: "border-pixel-gold/30 hover:border-pixel-gold/50"
}`}
>
<img
src={imageUrl}
alt={imageUrl}
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.style.display = "none";
}}
/>
{value === imageUrl && (
<div className="absolute inset-0 bg-pixel-gold/20 flex items-center justify-center">
<span className="text-pixel-gold text-xs"></span>
</div>
)}
</button>
))
)}
</div>
</div>
)}
</div>
);
}

View File

@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useBackgroundImage } from "@/hooks/usePreferences";
interface LeaderboardEntry {
rank: number;
@@ -18,6 +19,10 @@ const formatScore = (score: number): string => {
export default function LeaderboardSection() {
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([]);
const [loading, setLoading] = useState(true);
const backgroundImage = useBackgroundImage(
"leaderboard",
"/leaderboard-bg.jpg"
);
useEffect(() => {
fetch("/api/leaderboard")
@@ -45,7 +50,7 @@ export default function LeaderboardSection() {
<div
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('/leaderboard-bg.jpg')`,
backgroundImage: `url('${backgroundImage}')`,
}}
>
{/* Dark overlay for readability */}