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:
@@ -4,26 +4,25 @@ import { useEffect, useState } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Navigation from "@/components/Navigation";
|
||||
import ImageSelector from "@/components/ImageSelector";
|
||||
|
||||
interface UserPreferences {
|
||||
interface SitePreferences {
|
||||
id: string;
|
||||
userId: string;
|
||||
homeBackground: string | null;
|
||||
eventsBackground: string | null;
|
||||
leaderboardBackground: string | null;
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
type AdminSection = "preferences" | "users";
|
||||
|
||||
export default function AdminPage() {
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
const [preferences, setPreferences] = useState<UserPreferences[]>([]);
|
||||
const [activeSection, setActiveSection] =
|
||||
useState<AdminSection>("preferences");
|
||||
const [preferences, setPreferences] = useState<SitePreferences | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingUserId, setEditingUserId] = useState<string | null>(null);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
homeBackground: "",
|
||||
eventsBackground: "",
|
||||
@@ -52,6 +51,11 @@ export default function AdminPage() {
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setPreferences(data);
|
||||
setFormData({
|
||||
homeBackground: data.homeBackground || "",
|
||||
eventsBackground: data.eventsBackground || "",
|
||||
leaderboardBackground: data.leaderboardBackground || "",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching preferences:", error);
|
||||
@@ -60,38 +64,23 @@ export default function AdminPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (pref: UserPreferences) => {
|
||||
setEditingUserId(pref.userId);
|
||||
setFormData({
|
||||
homeBackground: pref.homeBackground || "",
|
||||
eventsBackground: pref.eventsBackground || "",
|
||||
leaderboardBackground: pref.leaderboardBackground || "",
|
||||
});
|
||||
const handleEdit = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editingUserId) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/admin/preferences", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: editingUserId,
|
||||
...formData,
|
||||
}),
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await fetchPreferences();
|
||||
setEditingUserId(null);
|
||||
setFormData({
|
||||
homeBackground: "",
|
||||
eventsBackground: "",
|
||||
leaderboardBackground: "",
|
||||
});
|
||||
setIsEditing(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating preferences:", error);
|
||||
@@ -99,12 +88,14 @@ export default function AdminPage() {
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditingUserId(null);
|
||||
setFormData({
|
||||
homeBackground: "",
|
||||
eventsBackground: "",
|
||||
leaderboardBackground: "",
|
||||
});
|
||||
setIsEditing(false);
|
||||
if (preferences) {
|
||||
setFormData({
|
||||
homeBackground: preferences.homeBackground || "",
|
||||
eventsBackground: preferences.eventsBackground || "",
|
||||
leaderboardBackground: preferences.leaderboardBackground || "",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (status === "loading" || loading) {
|
||||
@@ -125,27 +116,53 @@ export default function AdminPage() {
|
||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
|
||||
<h1 className="text-4xl font-gaming font-black mb-8 text-center">
|
||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
||||
ADMIN - GESTION DES PRÉFÉRENCES UI
|
||||
ADMIN
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
|
||||
<div className="space-y-4">
|
||||
{preferences.map((pref) => (
|
||||
<div
|
||||
key={pref.id}
|
||||
className="bg-black/60 border border-pixel-gold/20 rounded p-4"
|
||||
>
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex gap-4 mb-8 justify-center">
|
||||
<button
|
||||
onClick={() => setActiveSection("preferences")}
|
||||
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
|
||||
activeSection === "preferences"
|
||||
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
|
||||
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
|
||||
}`}
|
||||
>
|
||||
Préférences UI
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveSection("users")}
|
||||
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
|
||||
activeSection === "users"
|
||||
? "border-pixel-gold bg-pixel-gold/10 text-pixel-gold"
|
||||
: "border-pixel-gold/30 bg-black/60 text-gray-400 hover:border-pixel-gold/50"
|
||||
}`}
|
||||
>
|
||||
Utilisateurs
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{activeSection === "preferences" && (
|
||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
|
||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||
Préférences UI Globales
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-black/60 border border-pixel-gold/20 rounded p-4">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="text-pixel-gold font-bold text-lg">
|
||||
{pref.user.username}
|
||||
Images de fond du site
|
||||
</h3>
|
||||
<p className="text-gray-400 text-sm">{pref.user.email}</p>
|
||||
<p className="text-gray-400 text-sm">
|
||||
Ces préférences s'appliquent à tous les utilisateurs
|
||||
</p>
|
||||
</div>
|
||||
{editingUserId !== pref.userId && (
|
||||
{!isEditing && (
|
||||
<button
|
||||
onClick={() => handleEdit(pref)}
|
||||
onClick={handleEdit}
|
||||
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"
|
||||
>
|
||||
Modifier
|
||||
@@ -153,60 +170,39 @@ export default function AdminPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editingUserId === pref.userId ? (
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-1">
|
||||
Background Home
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.homeBackground}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
homeBackground: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="/got-2.jpg"
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-1">
|
||||
Background Events
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.eventsBackground}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
eventsBackground: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="/got-2.jpg"
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-1">
|
||||
Background Leaderboard
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.leaderboardBackground}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
leaderboardBackground: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="/leaderboard-bg.jpg"
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{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 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"
|
||||
@@ -224,30 +220,34 @@ export default function AdminPage() {
|
||||
) : (
|
||||
<div className="space-y-2 text-sm text-gray-400">
|
||||
<div>
|
||||
Home: {pref.homeBackground || "Par défaut"}
|
||||
Home: {preferences?.homeBackground || "Par défaut"}
|
||||
</div>
|
||||
<div>
|
||||
Events: {pref.eventsBackground || "Par défaut"}
|
||||
Events: {preferences?.eventsBackground || "Par défaut"}
|
||||
</div>
|
||||
<div>
|
||||
Leaderboard:{" "}
|
||||
{pref.leaderboardBackground || "Par défaut"}
|
||||
{preferences?.leaderboardBackground || "Par défaut"}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{preferences.length === 0 && (
|
||||
<div className="text-center text-gray-400 py-8">
|
||||
Aucune préférence trouvée
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === "users" && (
|
||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
|
||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||
Gestion des Utilisateurs
|
||||
</h2>
|
||||
<div className="text-center text-gray-400 py-8">
|
||||
Section utilisateurs à venir...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
48
app/api/admin/images/list/route.ts
Normal file
48
app/api/admin/images/list/route.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { Role } from "@/prisma/generated/prisma/client";
|
||||
import { readdir } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user || session.user.role !== Role.ADMIN) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
const images: string[] = [];
|
||||
|
||||
// Lister les images dans public/
|
||||
const publicDir = join(process.cwd(), "public");
|
||||
if (existsSync(publicDir)) {
|
||||
const files = await readdir(publicDir);
|
||||
const imageFiles = files.filter(
|
||||
(file) =>
|
||||
file.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i) && !file.startsWith(".")
|
||||
);
|
||||
images.push(...imageFiles.map((file) => `/${file}`));
|
||||
}
|
||||
|
||||
// Lister les images dans public/uploads/
|
||||
const uploadsDir = join(publicDir, "uploads");
|
||||
if (existsSync(uploadsDir)) {
|
||||
const uploadFiles = await readdir(uploadsDir);
|
||||
const imageFiles = uploadFiles.filter((file) =>
|
||||
file.match(/\.(jpg|jpeg|png|gif|webp|svg)$/i)
|
||||
);
|
||||
images.push(...imageFiles.map((file) => `/uploads/${file}`));
|
||||
}
|
||||
|
||||
return NextResponse.json({ images });
|
||||
} catch (error) {
|
||||
console.error("Error listing images:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la récupération des images" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
58
app/api/admin/images/upload/route.ts
Normal file
58
app/api/admin/images/upload/route.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { Role } from "@/prisma/generated/prisma/client";
|
||||
import { writeFile, mkdir } from "fs/promises";
|
||||
import { join } from "path";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user || session.user.role !== Role.ADMIN) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
const formData = await request.formData();
|
||||
const file = formData.get("file") as File;
|
||||
|
||||
if (!file) {
|
||||
return NextResponse.json({ error: "Aucun fichier fourni" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Vérifier le type de fichier
|
||||
if (!file.type.startsWith("image/")) {
|
||||
return NextResponse.json(
|
||||
{ error: "Le fichier doit être une image" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Créer le dossier uploads s'il n'existe pas
|
||||
const uploadsDir = join(process.cwd(), "public", "uploads");
|
||||
if (!existsSync(uploadsDir)) {
|
||||
await mkdir(uploadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Générer un nom de fichier unique
|
||||
const timestamp = Date.now();
|
||||
const filename = `${timestamp}-${file.name}`;
|
||||
const filepath = join(uploadsDir, filename);
|
||||
|
||||
// Convertir le fichier en buffer et l'écrire
|
||||
const bytes = await file.arrayBuffer();
|
||||
const buffer = Buffer.from(bytes);
|
||||
await writeFile(filepath, buffer);
|
||||
|
||||
// Retourner l'URL de l'image
|
||||
const imageUrl = `/uploads/${filename}`;
|
||||
return NextResponse.json({ url: imageUrl });
|
||||
} catch (error) {
|
||||
console.error("Error uploading image:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de l'upload de l'image" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,20 +11,24 @@ export async function GET() {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Récupérer toutes les préférences utilisateur
|
||||
const preferences = await prisma.userPreferences.findMany({
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
// Récupérer les préférences globales du site
|
||||
let sitePreferences = await prisma.sitePreferences.findUnique({
|
||||
where: { id: "global" },
|
||||
});
|
||||
|
||||
return NextResponse.json(preferences);
|
||||
// Si elles n'existent pas, créer une entrée par défaut
|
||||
if (!sitePreferences) {
|
||||
sitePreferences = await prisma.sitePreferences.create({
|
||||
data: {
|
||||
id: "global",
|
||||
homeBackground: null,
|
||||
eventsBackground: null,
|
||||
leaderboardBackground: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json(sitePreferences);
|
||||
} catch (error) {
|
||||
console.error("Error fetching admin preferences:", error);
|
||||
return NextResponse.json(
|
||||
@@ -43,25 +47,27 @@ export async function PUT(request: Request) {
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { userId, homeBackground, eventsBackground, leaderboardBackground } =
|
||||
body;
|
||||
const { homeBackground, eventsBackground, leaderboardBackground } = body;
|
||||
|
||||
if (!userId) {
|
||||
return NextResponse.json({ error: "userId requis" }, { status: 400 });
|
||||
}
|
||||
|
||||
const preferences = await prisma.userPreferences.upsert({
|
||||
where: { userId },
|
||||
const preferences = await prisma.sitePreferences.upsert({
|
||||
where: { id: "global" },
|
||||
update: {
|
||||
homeBackground: homeBackground ?? undefined,
|
||||
eventsBackground: eventsBackground ?? undefined,
|
||||
leaderboardBackground: leaderboardBackground ?? undefined,
|
||||
homeBackground:
|
||||
homeBackground === "" ? null : homeBackground ?? undefined,
|
||||
eventsBackground:
|
||||
eventsBackground === "" ? null : eventsBackground ?? undefined,
|
||||
leaderboardBackground:
|
||||
leaderboardBackground === ""
|
||||
? null
|
||||
: leaderboardBackground ?? undefined,
|
||||
},
|
||||
create: {
|
||||
userId,
|
||||
homeBackground: homeBackground ?? null,
|
||||
eventsBackground: eventsBackground ?? null,
|
||||
leaderboardBackground: leaderboardBackground ?? null,
|
||||
id: "global",
|
||||
homeBackground: homeBackground === "" ? null : homeBackground ?? null,
|
||||
eventsBackground:
|
||||
eventsBackground === "" ? null : eventsBackground ?? null,
|
||||
leaderboardBackground:
|
||||
leaderboardBackground === "" ? null : leaderboardBackground ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,65 +1,36 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { Role } from "@/prisma/generated/prisma/client";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
||||
}
|
||||
|
||||
const preferences = await prisma.userPreferences.findUnique({
|
||||
where: { userId: session.user.id },
|
||||
// Récupérer les préférences globales du site (pas besoin d'authentification)
|
||||
let sitePreferences = await prisma.sitePreferences.findUnique({
|
||||
where: { id: "global" },
|
||||
});
|
||||
|
||||
return NextResponse.json(preferences || {});
|
||||
// Si elles n'existent pas, retourner des valeurs par défaut
|
||||
if (!sitePreferences) {
|
||||
return NextResponse.json({
|
||||
homeBackground: null,
|
||||
eventsBackground: null,
|
||||
leaderboardBackground: null,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
homeBackground: sitePreferences.homeBackground,
|
||||
eventsBackground: sitePreferences.eventsBackground,
|
||||
leaderboardBackground: sitePreferences.leaderboardBackground,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching preferences:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la récupération des préférences" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 { homeBackground, eventsBackground, leaderboardBackground, theme } =
|
||||
body;
|
||||
|
||||
const preferences = await prisma.userPreferences.upsert({
|
||||
where: { userId: session.user.id },
|
||||
update: {
|
||||
homeBackground: homeBackground ?? undefined,
|
||||
eventsBackground: eventsBackground ?? undefined,
|
||||
leaderboardBackground: leaderboardBackground ?? undefined,
|
||||
theme: theme ?? undefined,
|
||||
},
|
||||
create: {
|
||||
userId: session.user.id,
|
||||
homeBackground: homeBackground ?? null,
|
||||
eventsBackground: eventsBackground ?? null,
|
||||
leaderboardBackground: leaderboardBackground ?? null,
|
||||
theme: theme ?? "default",
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(preferences);
|
||||
} catch (error) {
|
||||
console.error("Error updating preferences:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la mise à jour des préférences" },
|
||||
{ status: 500 }
|
||||
{
|
||||
homeBackground: null,
|
||||
eventsBackground: null,
|
||||
leaderboardBackground: null,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user