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
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 33s
This commit is contained in:
399
components/BackgroundPreferences.tsx
Normal file
399
components/BackgroundPreferences.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user