Enhance image upload and background management: Update Docker configuration to create a dedicated backgrounds directory for uploaded images, modify API routes to handle background images specifically, and improve README documentation to reflect these changes. Additionally, refactor components to utilize the new Avatar component for consistent avatar rendering across the application.
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 33s

This commit is contained in:
Julien Froidefond
2025-12-12 08:46:31 +01:00
parent 3ad680f416
commit ae08ed7793
24 changed files with 1100 additions and 464 deletions

View File

@@ -0,0 +1,399 @@
"use client";
import { useState, useEffect } from "react";
import ImageSelector from "@/components/ImageSelector";
interface SitePreferences {
id: string;
homeBackground: string | null;
eventsBackground: string | null;
leaderboardBackground: string | null;
}
interface BackgroundPreferencesProps {
initialPreferences: SitePreferences;
}
const DEFAULT_IMAGES = {
home: "/got-2.jpg",
events: "/got-2.jpg",
leaderboard: "/leaderboard-bg.jpg",
};
export default function BackgroundPreferences({
initialPreferences,
}: BackgroundPreferencesProps) {
const [preferences, setPreferences] = useState<SitePreferences | null>(
initialPreferences
);
const [isEditing, setIsEditing] = useState(false);
// Helper pour obtenir la valeur à afficher dans le formulaire
const getFormValue = (
dbValue: string | null | undefined,
defaultImage: string
) => {
return dbValue && dbValue.trim() !== "" ? dbValue : defaultImage;
};
// Helper pour obtenir la valeur à envoyer à l'API
const getApiValue = (formValue: string, defaultImage: string) => {
return formValue === defaultImage ? "" : formValue;
};
const [formData, setFormData] = useState({
homeBackground: getFormValue(
initialPreferences.homeBackground,
DEFAULT_IMAGES.home
),
eventsBackground: getFormValue(
initialPreferences.eventsBackground,
DEFAULT_IMAGES.events
),
leaderboardBackground: getFormValue(
initialPreferences.leaderboardBackground,
DEFAULT_IMAGES.leaderboard
),
});
// Synchroniser les préférences quand initialPreferences change
useEffect(() => {
setPreferences(initialPreferences);
setFormData({
homeBackground: getFormValue(
initialPreferences.homeBackground,
DEFAULT_IMAGES.home
),
eventsBackground: getFormValue(
initialPreferences.eventsBackground,
DEFAULT_IMAGES.events
),
leaderboardBackground: getFormValue(
initialPreferences.leaderboardBackground,
DEFAULT_IMAGES.leaderboard
),
});
}, [initialPreferences]);
const handleEdit = () => {
setIsEditing(true);
};
const handleSave = async () => {
try {
// Convertir les valeurs du formulaire en valeurs API ("" si c'est l'image par défaut)
const apiData = {
homeBackground: getApiValue(
formData.homeBackground,
DEFAULT_IMAGES.home
),
eventsBackground: getApiValue(
formData.eventsBackground,
DEFAULT_IMAGES.events
),
leaderboardBackground: getApiValue(
formData.leaderboardBackground,
DEFAULT_IMAGES.leaderboard
),
};
const response = await fetch("/api/admin/preferences", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(apiData),
});
if (response.ok) {
const data = await response.json();
setPreferences(data);
// Réinitialiser formData avec les nouvelles valeurs (ou images par défaut)
setFormData({
homeBackground: getFormValue(
data.homeBackground,
DEFAULT_IMAGES.home
),
eventsBackground: getFormValue(
data.eventsBackground,
DEFAULT_IMAGES.events
),
leaderboardBackground: getFormValue(
data.leaderboardBackground,
DEFAULT_IMAGES.leaderboard
),
});
setIsEditing(false);
} else {
const errorData = await response.json();
console.error("Error updating preferences:", errorData);
alert(errorData.error || "Erreur lors de la mise à jour");
}
} catch (error) {
console.error("Error updating preferences:", error);
}
};
const handleCancel = () => {
setIsEditing(false);
if (preferences) {
setFormData({
homeBackground: getFormValue(
preferences.homeBackground,
DEFAULT_IMAGES.home
),
eventsBackground: getFormValue(
preferences.eventsBackground,
DEFAULT_IMAGES.events
),
leaderboardBackground: getFormValue(
preferences.leaderboardBackground,
DEFAULT_IMAGES.leaderboard
),
});
}
};
return (
<div className="bg-black/60 border border-pixel-gold/20 rounded p-3 sm:p-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4">
<div className="min-w-0 flex-1">
<h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
Images de fond du site
</h3>
<p className="text-gray-400 text-xs sm:text-sm">
Ces préférences s&apos;appliquent à tous les utilisateurs
</p>
</div>
{!isEditing && (
<button
onClick={handleEdit}
className="px-3 sm:px-4 py-2 border border-pixel-gold/50 bg-black/60 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition whitespace-nowrap flex-shrink-0"
>
Modifier
</button>
)}
</div>
{isEditing ? (
<div className="space-y-6">
<ImageSelector
value={formData.homeBackground}
onChange={(url) =>
setFormData({
...formData,
homeBackground: url,
})
}
label="Background Home"
/>
<ImageSelector
value={formData.eventsBackground}
onChange={(url) =>
setFormData({
...formData,
eventsBackground: url,
})
}
label="Background Events"
/>
<ImageSelector
value={formData.leaderboardBackground}
onChange={(url) =>
setFormData({
...formData,
leaderboardBackground: url,
})
}
label="Background Leaderboard"
/>
<div className="flex flex-col sm:flex-row gap-2 pt-4">
<button
onClick={handleSave}
className="px-4 py-2 border border-green-500/50 bg-green-900/20 text-green-400 uppercase text-xs tracking-widest rounded hover:bg-green-900/30 transition"
>
Enregistrer
</button>
<button
onClick={handleCancel}
className="px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/30 transition"
>
Annuler
</button>
</div>
</div>
) : (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<span className="text-pixel-gold font-bold text-sm sm:text-base min-w-0 sm:min-w-[120px] flex-shrink-0">
Home:
</span>
{(() => {
const currentImage =
preferences?.homeBackground &&
preferences.homeBackground.trim() !== ""
? preferences.homeBackground
: DEFAULT_IMAGES.home;
const isDefault =
!preferences?.homeBackground ||
preferences.homeBackground.trim() === "";
return (
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
<div className="relative w-16 h-10 sm:w-20 sm:h-12 rounded border border-pixel-gold/30 overflow-hidden bg-black/60 flex-shrink-0">
<img
src={currentImage}
alt="Home background"
className="w-full h-full object-cover"
onError={(e) => {
const target = e.currentTarget;
const currentSrc = target.src;
const fallbackSrc = "/got-2.jpg";
if (!currentSrc.includes(fallbackSrc)) {
target.src = fallbackSrc;
} else {
target.style.display = "none";
const fallbackDiv =
target.nextElementSibling as HTMLElement;
if (fallbackDiv) {
fallbackDiv.classList.remove("hidden");
}
}
}}
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/60 text-gray-500 text-xs hidden">
No image
</div>
</div>
<div className="flex flex-col min-w-0 flex-1">
<span className="text-xs text-gray-400 truncate min-w-0">
{isDefault ? "Par défaut: " : ""}
{currentImage}
</span>
{isDefault && (
<span className="text-[10px] text-gray-500 italic">
(Image par défaut)
</span>
)}
</div>
</div>
);
})()}
</div>
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<span className="text-pixel-gold font-bold text-sm sm:text-base min-w-0 sm:min-w-[120px] flex-shrink-0">
Events:
</span>
{(() => {
const currentImage =
preferences?.eventsBackground &&
preferences.eventsBackground.trim() !== ""
? preferences.eventsBackground
: DEFAULT_IMAGES.events;
const isDefault =
!preferences?.eventsBackground ||
preferences.eventsBackground.trim() === "";
return (
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
<div className="relative w-16 h-10 sm:w-20 sm:h-12 rounded border border-pixel-gold/30 overflow-hidden bg-black/60 flex-shrink-0">
<img
src={currentImage}
alt="Events background"
className="w-full h-full object-cover"
onError={(e) => {
const target = e.currentTarget;
const currentSrc = target.src;
const fallbackSrc = "/got-2.jpg";
if (!currentSrc.includes(fallbackSrc)) {
target.src = fallbackSrc;
} else {
target.style.display = "none";
const fallbackDiv =
target.nextElementSibling as HTMLElement;
if (fallbackDiv) {
fallbackDiv.classList.remove("hidden");
}
}
}}
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/60 text-gray-500 text-xs hidden">
No image
</div>
</div>
<div className="flex flex-col min-w-0 flex-1">
<span className="text-xs text-gray-400 truncate min-w-0">
{isDefault ? "Par défaut: " : ""}
{currentImage}
</span>
{isDefault && (
<span className="text-[10px] text-gray-500 italic">
(Image par défaut)
</span>
)}
</div>
</div>
);
})()}
</div>
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
<span className="text-pixel-gold font-bold text-sm sm:text-base min-w-0 sm:min-w-[120px] flex-shrink-0">
Leaderboard:
</span>
{(() => {
const currentImage =
preferences?.leaderboardBackground &&
preferences.leaderboardBackground.trim() !== ""
? preferences.leaderboardBackground
: DEFAULT_IMAGES.leaderboard;
const isDefault =
!preferences?.leaderboardBackground ||
preferences.leaderboardBackground.trim() === "";
return (
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
<div className="relative w-16 h-10 sm:w-20 sm:h-12 rounded border border-pixel-gold/30 overflow-hidden bg-black/60 flex-shrink-0">
<img
src={currentImage}
alt="Leaderboard background"
className="w-full h-full object-cover"
onError={(e) => {
const target = e.currentTarget;
const currentSrc = target.src;
const fallbackSrc = "/got-2.jpg";
if (!currentSrc.includes(fallbackSrc)) {
target.src = fallbackSrc;
} else {
target.style.display = "none";
const fallbackDiv =
target.nextElementSibling as HTMLElement;
if (fallbackDiv) {
fallbackDiv.classList.remove("hidden");
}
}
}}
/>
<div className="absolute inset-0 flex items-center justify-center bg-black/60 text-gray-500 text-xs hidden">
No image
</div>
</div>
<div className="flex flex-col min-w-0 flex-1">
<span className="text-xs text-gray-400 truncate min-w-0">
{isDefault ? "Par défaut: " : ""}
{currentImage}
</span>
{isDefault && (
<span className="text-[10px] text-gray-500 italic">
(Image par défaut)
</span>
)}
</div>
</div>
);
})()}
</div>
</div>
)}
</div>
);
}