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

@@ -1,6 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import Avatar from "./Avatar";
interface User {
id: string;
@@ -19,6 +20,8 @@ interface User {
interface EditingUser {
userId: string;
username: string | null;
avatar: string | null;
hpDelta: number;
xpDelta: number;
score: number | null;
@@ -32,6 +35,7 @@ export default function UserManagement() {
const [editingUser, setEditingUser] = useState<EditingUser | null>(null);
const [saving, setSaving] = useState(false);
const [deletingUserId, setDeletingUserId] = useState<string | null>(null);
const [uploadingAvatar, setUploadingAvatar] = useState<string | null>(null);
useEffect(() => {
fetchUsers();
@@ -54,6 +58,8 @@ export default function UserManagement() {
const handleEdit = (user: User) => {
setEditingUser({
userId: user.id,
username: user.username,
avatar: user.avatar,
hpDelta: 0,
xpDelta: 0,
score: user.score,
@@ -68,6 +74,8 @@ export default function UserManagement() {
setSaving(true);
try {
const body: {
username?: string;
avatar?: string | null;
hpDelta?: number;
xpDelta?: number;
score?: number;
@@ -75,6 +83,12 @@ export default function UserManagement() {
role?: string;
} = {};
if (editingUser.username !== null) {
body.username = editingUser.username;
}
if (editingUser.avatar !== undefined) {
body.avatar = editingUser.avatar;
}
if (editingUser.hpDelta !== 0) {
body.hpDelta = editingUser.hpDelta;
}
@@ -170,6 +184,10 @@ export default function UserManagement() {
const previewXp = isEditing
? Math.max(0, user.xp + editingUser.xpDelta)
: user.xp;
const displayAvatar = isEditing ? editingUser.avatar : user.avatar;
const displayUsername = isEditing
? editingUser.username || user.username
: user.username;
return (
<div
@@ -179,32 +197,17 @@ export default function UserManagement() {
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-2">
<div className="flex gap-2 sm:gap-3 items-center flex-1 min-w-0">
{/* Avatar */}
<div className="w-8 h-8 sm:w-10 sm:h-10 rounded-full border-2 border-pixel-gold/50 overflow-hidden bg-black/60 flex-shrink-0">
{user.avatar ? (
<img
src={user.avatar}
alt={user.username}
className="w-full h-full object-cover"
onError={(e) => {
e.currentTarget.style.display = "none";
e.currentTarget.nextElementSibling?.classList.remove(
"hidden"
);
}}
/>
) : null}
<div
className={`w-full h-full flex items-center justify-center text-pixel-gold text-xs sm:text-sm font-bold ${
user.avatar ? "hidden" : ""
}`}
>
{user.username.charAt(0).toUpperCase()}
</div>
</div>
<Avatar
src={displayAvatar}
username={displayUsername}
size="sm"
className="flex-shrink-0"
borderClassName="border-2 border-pixel-gold/50"
/>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 sm:gap-2 flex-wrap">
<h3 className="text-pixel-gold font-bold text-sm sm:text-base break-words">
{user.username}
{displayUsername}
</h3>
<span className="text-[10px] sm:text-xs text-gray-500 whitespace-nowrap">
Niveau {user.level}
@@ -250,6 +253,142 @@ export default function UserManagement() {
{isEditing ? (
<div className="space-y-4">
{/* Username Section */}
<div>
<label className="block text-xs sm:text-sm text-gray-300 mb-2">
Nom d&apos;utilisateur
</label>
<input
type="text"
value={editingUser.username || ""}
onChange={(e) =>
setEditingUser({
...editingUser,
username: e.target.value,
})
}
className="w-full px-2 sm:px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-xs sm:text-sm"
placeholder="Nom d'utilisateur"
/>
</div>
{/* Avatar Section */}
<div className="flex flex-col items-center gap-3">
<label className="block text-xs sm:text-sm text-gray-300 mb-2">
Avatar
</label>
{/* Preview */}
<div className="relative">
<Avatar
src={editingUser.avatar}
username={editingUser.username || user.username}
size="lg"
borderClassName="border-2 border-pixel-gold/50"
/>
{uploadingAvatar === user.id && (
<div className="absolute inset-0 bg-black/60 flex items-center justify-center rounded-full">
<div className="text-pixel-gold text-xs">
Upload...
</div>
</div>
)}
</div>
{/* Avatars par défaut */}
<div className="flex flex-col items-center gap-2 w-full">
<label className="block text-pixel-gold text-[10px] sm:text-xs uppercase tracking-widest">
Avatars par défaut
</label>
<div className="flex flex-wrap gap-2 justify-center">
{[
"/avatar-1.jpg",
"/avatar-2.jpg",
"/avatar-3.jpg",
"/avatar-4.jpg",
"/avatar-5.jpg",
"/avatar-6.jpg",
].map((defaultAvatar) => (
<button
key={defaultAvatar}
type="button"
onClick={() =>
setEditingUser({
...editingUser,
avatar: defaultAvatar,
})
}
className={`w-12 h-12 sm:w-14 sm:h-14 rounded-full border-2 overflow-hidden transition ${
editingUser.avatar === defaultAvatar
? "border-pixel-gold scale-110"
: "border-pixel-gold/30 hover:border-pixel-gold/50"
}`}
>
<img
src={defaultAvatar}
alt="Avatar par défaut"
className="w-full h-full object-cover"
/>
</button>
))}
</div>
</div>
{/* Custom Upload */}
<div>
<input
type="file"
accept="image/*"
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
setUploadingAvatar(user.id);
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();
setEditingUser({
...editingUser,
avatar: data.url,
});
} 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 {
setUploadingAvatar(null);
if (e.target) {
e.target.value = "";
}
}
}}
className="hidden"
id={`avatar-upload-${user.id}`}
/>
<label
htmlFor={`avatar-upload-${user.id}`}
className="px-3 sm:px-4 py-1.5 border border-pixel-gold/50 bg-black/40 text-white uppercase text-[10px] sm:text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition cursor-pointer inline-block"
>
{uploadingAvatar === user.id
? "Upload en cours..."
: "Upload un avatar custom"}
</label>
</div>
</div>
{/* HP Section */}
<div>
<div className="flex justify-between items-center mb-2">