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 { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Navigation from "@/components/Navigation";
|
import Navigation from "@/components/Navigation";
|
||||||
|
import ImageSelector from "@/components/ImageSelector";
|
||||||
|
|
||||||
interface UserPreferences {
|
interface SitePreferences {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
homeBackground: string | null;
|
homeBackground: string | null;
|
||||||
eventsBackground: string | null;
|
eventsBackground: string | null;
|
||||||
leaderboardBackground: string | null;
|
leaderboardBackground: string | null;
|
||||||
user: {
|
|
||||||
id: string;
|
|
||||||
username: string;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AdminSection = "preferences" | "users";
|
||||||
|
|
||||||
export default function AdminPage() {
|
export default function AdminPage() {
|
||||||
const { data: session, status } = useSession();
|
const { data: session, status } = useSession();
|
||||||
const router = useRouter();
|
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 [loading, setLoading] = useState(true);
|
||||||
const [editingUserId, setEditingUserId] = useState<string | null>(null);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
homeBackground: "",
|
homeBackground: "",
|
||||||
eventsBackground: "",
|
eventsBackground: "",
|
||||||
@@ -52,6 +51,11 @@ export default function AdminPage() {
|
|||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setPreferences(data);
|
setPreferences(data);
|
||||||
|
setFormData({
|
||||||
|
homeBackground: data.homeBackground || "",
|
||||||
|
eventsBackground: data.eventsBackground || "",
|
||||||
|
leaderboardBackground: data.leaderboardBackground || "",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching preferences:", error);
|
console.error("Error fetching preferences:", error);
|
||||||
@@ -60,38 +64,23 @@ export default function AdminPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (pref: UserPreferences) => {
|
const handleEdit = () => {
|
||||||
setEditingUserId(pref.userId);
|
setIsEditing(true);
|
||||||
setFormData({
|
|
||||||
homeBackground: pref.homeBackground || "",
|
|
||||||
eventsBackground: pref.eventsBackground || "",
|
|
||||||
leaderboardBackground: pref.leaderboardBackground || "",
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
if (!editingUserId) return;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/admin/preferences", {
|
const response = await fetch("/api/admin/preferences", {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify(formData),
|
||||||
userId: editingUserId,
|
|
||||||
...formData,
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
await fetchPreferences();
|
await fetchPreferences();
|
||||||
setEditingUserId(null);
|
setIsEditing(false);
|
||||||
setFormData({
|
|
||||||
homeBackground: "",
|
|
||||||
eventsBackground: "",
|
|
||||||
leaderboardBackground: "",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating preferences:", error);
|
console.error("Error updating preferences:", error);
|
||||||
@@ -99,12 +88,14 @@ export default function AdminPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleCancel = () => {
|
const handleCancel = () => {
|
||||||
setEditingUserId(null);
|
setIsEditing(false);
|
||||||
setFormData({
|
if (preferences) {
|
||||||
homeBackground: "",
|
setFormData({
|
||||||
eventsBackground: "",
|
homeBackground: preferences.homeBackground || "",
|
||||||
leaderboardBackground: "",
|
eventsBackground: preferences.eventsBackground || "",
|
||||||
});
|
leaderboardBackground: preferences.leaderboardBackground || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (status === "loading" || loading) {
|
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">
|
<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">
|
<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">
|
<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>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-6 backdrop-blur-sm">
|
{/* Navigation Tabs */}
|
||||||
<div className="space-y-4">
|
<div className="flex gap-4 mb-8 justify-center">
|
||||||
{preferences.map((pref) => (
|
<button
|
||||||
<div
|
onClick={() => setActiveSection("preferences")}
|
||||||
key={pref.id}
|
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
|
||||||
className="bg-black/60 border border-pixel-gold/20 rounded p-4"
|
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 className="flex justify-between items-start mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-pixel-gold font-bold text-lg">
|
<h3 className="text-pixel-gold font-bold text-lg">
|
||||||
{pref.user.username}
|
Images de fond du site
|
||||||
</h3>
|
</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>
|
</div>
|
||||||
{editingUserId !== pref.userId && (
|
{!isEditing && (
|
||||||
<button
|
<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"
|
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
|
Modifier
|
||||||
@@ -153,60 +170,39 @@ export default function AdminPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{editingUserId === pref.userId ? (
|
{isEditing ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-6">
|
||||||
<div>
|
<ImageSelector
|
||||||
<label className="block text-sm text-gray-300 mb-1">
|
value={formData.homeBackground}
|
||||||
Background Home
|
onChange={(url) =>
|
||||||
</label>
|
setFormData({
|
||||||
<input
|
...formData,
|
||||||
type="text"
|
homeBackground: url,
|
||||||
value={formData.homeBackground}
|
})
|
||||||
onChange={(e) =>
|
}
|
||||||
setFormData({
|
label="Background Home"
|
||||||
...formData,
|
/>
|
||||||
homeBackground: e.target.value,
|
<ImageSelector
|
||||||
})
|
value={formData.eventsBackground}
|
||||||
}
|
onChange={(url) =>
|
||||||
placeholder="/got-2.jpg"
|
setFormData({
|
||||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm"
|
...formData,
|
||||||
/>
|
eventsBackground: url,
|
||||||
</div>
|
})
|
||||||
<div>
|
}
|
||||||
<label className="block text-sm text-gray-300 mb-1">
|
label="Background Events"
|
||||||
Background Events
|
/>
|
||||||
</label>
|
<ImageSelector
|
||||||
<input
|
value={formData.leaderboardBackground}
|
||||||
type="text"
|
onChange={(url) =>
|
||||||
value={formData.eventsBackground}
|
setFormData({
|
||||||
onChange={(e) =>
|
...formData,
|
||||||
setFormData({
|
leaderboardBackground: url,
|
||||||
...formData,
|
})
|
||||||
eventsBackground: e.target.value,
|
}
|
||||||
})
|
label="Background Leaderboard"
|
||||||
}
|
/>
|
||||||
placeholder="/got-2.jpg"
|
<div className="flex gap-2 pt-4">
|
||||||
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">
|
|
||||||
<button
|
<button
|
||||||
onClick={handleSave}
|
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"
|
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 className="space-y-2 text-sm text-gray-400">
|
||||||
<div>
|
<div>
|
||||||
Home: {pref.homeBackground || "Par défaut"}
|
Home: {preferences?.homeBackground || "Par défaut"}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Events: {pref.eventsBackground || "Par défaut"}
|
Events: {preferences?.eventsBackground || "Par défaut"}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
Leaderboard:{" "}
|
Leaderboard:{" "}
|
||||||
{pref.leaderboardBackground || "Par défaut"}
|
{preferences?.leaderboardBackground || "Par défaut"}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</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 });
|
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer toutes les préférences utilisateur
|
// Récupérer les préférences globales du site
|
||||||
const preferences = await prisma.userPreferences.findMany({
|
let sitePreferences = await prisma.sitePreferences.findUnique({
|
||||||
include: {
|
where: { id: "global" },
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
email: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error("Error fetching admin preferences:", error);
|
console.error("Error fetching admin preferences:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -43,25 +47,27 @@ export async function PUT(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { userId, homeBackground, eventsBackground, leaderboardBackground } =
|
const { homeBackground, eventsBackground, leaderboardBackground } = body;
|
||||||
body;
|
|
||||||
|
|
||||||
if (!userId) {
|
const preferences = await prisma.sitePreferences.upsert({
|
||||||
return NextResponse.json({ error: "userId requis" }, { status: 400 });
|
where: { id: "global" },
|
||||||
}
|
|
||||||
|
|
||||||
const preferences = await prisma.userPreferences.upsert({
|
|
||||||
where: { userId },
|
|
||||||
update: {
|
update: {
|
||||||
homeBackground: homeBackground ?? undefined,
|
homeBackground:
|
||||||
eventsBackground: eventsBackground ?? undefined,
|
homeBackground === "" ? null : homeBackground ?? undefined,
|
||||||
leaderboardBackground: leaderboardBackground ?? undefined,
|
eventsBackground:
|
||||||
|
eventsBackground === "" ? null : eventsBackground ?? undefined,
|
||||||
|
leaderboardBackground:
|
||||||
|
leaderboardBackground === ""
|
||||||
|
? null
|
||||||
|
: leaderboardBackground ?? undefined,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
userId,
|
id: "global",
|
||||||
homeBackground: homeBackground ?? null,
|
homeBackground: homeBackground === "" ? null : homeBackground ?? null,
|
||||||
eventsBackground: eventsBackground ?? null,
|
eventsBackground:
|
||||||
leaderboardBackground: leaderboardBackground ?? null,
|
eventsBackground === "" ? null : eventsBackground ?? null,
|
||||||
|
leaderboardBackground:
|
||||||
|
leaderboardBackground === "" ? null : leaderboardBackground ?? null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +1,36 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { auth } from "@/lib/auth";
|
|
||||||
import { prisma } from "@/lib/prisma";
|
import { prisma } from "@/lib/prisma";
|
||||||
import { Role } from "@/prisma/generated/prisma/client";
|
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const session = await auth();
|
// Récupérer les préférences globales du site (pas besoin d'authentification)
|
||||||
|
let sitePreferences = await prisma.sitePreferences.findUnique({
|
||||||
if (!session?.user) {
|
where: { id: "global" },
|
||||||
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const preferences = await prisma.userPreferences.findUnique({
|
|
||||||
where: { userId: session.user.id },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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) {
|
} catch (error) {
|
||||||
console.error("Error fetching preferences:", error);
|
console.error("Error fetching preferences:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: "Erreur lors de la récupération des préférences" },
|
{
|
||||||
{ status: 500 }
|
homeBackground: null,
|
||||||
);
|
eventsBackground: null,
|
||||||
}
|
leaderboardBackground: null,
|
||||||
}
|
},
|
||||||
|
{ status: 200 }
|
||||||
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 }
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useBackgroundImage } from "@/hooks/usePreferences";
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -67,6 +68,7 @@ const getStatusBadge = (status: Event["status"]) => {
|
|||||||
export default function EventsPageSection() {
|
export default function EventsPageSection() {
|
||||||
const [events, setEvents] = useState<Event[]>([]);
|
const [events, setEvents] = useState<Event[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const backgroundImage = useBackgroundImage("events", "/got-2.jpg");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/events")
|
fetch("/api/events")
|
||||||
@@ -94,7 +96,7 @@ export default function EventsPageSection() {
|
|||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url('/got-2.jpg')`,
|
backgroundImage: `url('${backgroundImage}')`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Dark overlay for readability */}
|
{/* Dark overlay for readability */}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useBackgroundImage } from "@/hooks/usePreferences";
|
||||||
|
|
||||||
export default function HeroSection() {
|
export default function HeroSection() {
|
||||||
|
const backgroundImage = useBackgroundImage("home", "/got-2.jpg");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url('/got-2.jpg')`,
|
backgroundImage: `url('${backgroundImage}')`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Dark overlay for readability */}
|
{/* Dark overlay for readability */}
|
||||||
|
|||||||
194
components/ImageSelector.tsx
Normal file
194
components/ImageSelector.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import { useBackgroundImage } from "@/hooks/usePreferences";
|
||||||
|
|
||||||
interface LeaderboardEntry {
|
interface LeaderboardEntry {
|
||||||
rank: number;
|
rank: number;
|
||||||
@@ -18,6 +19,10 @@ const formatScore = (score: number): string => {
|
|||||||
export default function LeaderboardSection() {
|
export default function LeaderboardSection() {
|
||||||
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([]);
|
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const backgroundImage = useBackgroundImage(
|
||||||
|
"leaderboard",
|
||||||
|
"/leaderboard-bg.jpg"
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch("/api/leaderboard")
|
fetch("/api/leaderboard")
|
||||||
@@ -45,7 +50,7 @@ export default function LeaderboardSection() {
|
|||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url('/leaderboard-bg.jpg')`,
|
backgroundImage: `url('${backgroundImage}')`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Dark overlay for readability */}
|
{/* Dark overlay for readability */}
|
||||||
|
|||||||
56
hooks/usePreferences.ts
Normal file
56
hooks/usePreferences.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Preferences {
|
||||||
|
homeBackground: string | null;
|
||||||
|
eventsBackground: string | null;
|
||||||
|
leaderboardBackground: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePreferences() {
|
||||||
|
const [preferences, setPreferences] = useState<Preferences | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Les préférences sont maintenant globales, pas besoin d'authentification
|
||||||
|
fetch("/api/preferences")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data) => {
|
||||||
|
setPreferences(
|
||||||
|
data || {
|
||||||
|
homeBackground: null,
|
||||||
|
eventsBackground: null,
|
||||||
|
leaderboardBackground: null,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setPreferences({
|
||||||
|
homeBackground: null,
|
||||||
|
eventsBackground: null,
|
||||||
|
leaderboardBackground: null,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return { preferences, loading };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useBackgroundImage(
|
||||||
|
page: "home" | "events" | "leaderboard",
|
||||||
|
defaultImage: string
|
||||||
|
) {
|
||||||
|
const { preferences } = usePreferences();
|
||||||
|
const [backgroundImage, setBackgroundImage] = useState(defaultImage);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (preferences) {
|
||||||
|
const imageKey = `${page}Background` as keyof Preferences;
|
||||||
|
const customImage = preferences[imageKey];
|
||||||
|
setBackgroundImage(customImage || defaultImage);
|
||||||
|
}
|
||||||
|
}, [preferences, page, defaultImage]);
|
||||||
|
|
||||||
|
return backgroundImage;
|
||||||
|
}
|
||||||
@@ -32,3 +32,8 @@ export type UserPreferences = Prisma.UserPreferencesModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Event = Prisma.EventModel
|
export type Event = Prisma.EventModel
|
||||||
|
/**
|
||||||
|
* Model SitePreferences
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type SitePreferences = Prisma.SitePreferencesModel
|
||||||
|
|||||||
@@ -54,3 +54,8 @@ export type UserPreferences = Prisma.UserPreferencesModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Event = Prisma.EventModel
|
export type Event = Prisma.EventModel
|
||||||
|
/**
|
||||||
|
* Model SitePreferences
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type SitePreferences = Prisma.SitePreferencesModel
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const config: runtime.GetPrismaClientConfig = {
|
|||||||
"clientVersion": "7.1.0",
|
"clientVersion": "7.1.0",
|
||||||
"engineVersion": "ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba",
|
"engineVersion": "ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba",
|
||||||
"activeProvider": "sqlite",
|
"activeProvider": "sqlite",
|
||||||
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"./generated/prisma\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nenum Role {\n USER\n ADMIN\n}\n\nenum EventType {\n SUMMIT\n LAUNCH\n FESTIVAL\n COMPETITION\n}\n\nenum EventStatus {\n UPCOMING\n LIVE\n PAST\n}\n\nmodel User {\n id String @id @default(cuid())\n email String @unique\n password String\n username String @unique\n role Role @default(USER)\n score Int @default(0)\n level Int @default(1)\n hp Int @default(1000)\n maxHp Int @default(1000)\n xp Int @default(0)\n maxXp Int @default(5000)\n avatar String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n preferences UserPreferences?\n\n @@index([score])\n @@index([email])\n}\n\nmodel UserPreferences {\n id String @id @default(cuid())\n userId String @unique\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n // Background images for each page\n homeBackground String?\n eventsBackground String?\n leaderboardBackground String?\n\n // Other UI preferences can be added here\n theme String? @default(\"default\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Event {\n id String @id @default(cuid())\n date String\n name String\n description String\n type EventType\n status EventStatus\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([status])\n @@index([date])\n}\n",
|
"inlineSchema": "// This is your Prisma schema file,\n// learn more about it in the docs: https://pris.ly/d/prisma-schema\n\ngenerator client {\n provider = \"prisma-client\"\n output = \"./generated/prisma\"\n}\n\ndatasource db {\n provider = \"sqlite\"\n}\n\nenum Role {\n USER\n ADMIN\n}\n\nenum EventType {\n SUMMIT\n LAUNCH\n FESTIVAL\n COMPETITION\n}\n\nenum EventStatus {\n UPCOMING\n LIVE\n PAST\n}\n\nmodel User {\n id String @id @default(cuid())\n email String @unique\n password String\n username String @unique\n role Role @default(USER)\n score Int @default(0)\n level Int @default(1)\n hp Int @default(1000)\n maxHp Int @default(1000)\n xp Int @default(0)\n maxXp Int @default(5000)\n avatar String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n preferences UserPreferences?\n\n @@index([score])\n @@index([email])\n}\n\nmodel UserPreferences {\n id String @id @default(cuid())\n userId String @unique\n user User @relation(fields: [userId], references: [id], onDelete: Cascade)\n\n // Background images for each page\n homeBackground String?\n eventsBackground String?\n leaderboardBackground String?\n\n // Other UI preferences can be added here\n theme String? @default(\"default\")\n\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel Event {\n id String @id @default(cuid())\n date String\n name String\n description String\n type EventType\n status EventStatus\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([status])\n @@index([date])\n}\n\nmodel SitePreferences {\n id String @id @default(\"global\")\n homeBackground String?\n eventsBackground String?\n leaderboardBackground String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n",
|
||||||
"runtimeDataModel": {
|
"runtimeDataModel": {
|
||||||
"models": {},
|
"models": {},
|
||||||
"enums": {},
|
"enums": {},
|
||||||
@@ -28,7 +28,7 @@ const config: runtime.GetPrismaClientConfig = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"Role\"},{\"name\":\"score\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"level\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"hp\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"maxHp\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"xp\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"maxXp\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"avatar\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"preferences\",\"kind\":\"object\",\"type\":\"UserPreferences\",\"relationName\":\"UserToUserPreferences\"}],\"dbName\":null},\"UserPreferences\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserPreferences\"},{\"name\":\"homeBackground\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"eventsBackground\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"leaderboardBackground\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"theme\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Event\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"date\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"enum\",\"type\":\"EventType\"},{\"name\":\"status\",\"kind\":\"enum\",\"type\":\"EventStatus\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"username\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"role\",\"kind\":\"enum\",\"type\":\"Role\"},{\"name\":\"score\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"level\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"hp\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"maxHp\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"xp\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"maxXp\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"avatar\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"preferences\",\"kind\":\"object\",\"type\":\"UserPreferences\",\"relationName\":\"UserToUserPreferences\"}],\"dbName\":null},\"UserPreferences\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"user\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"UserToUserPreferences\"},{\"name\":\"homeBackground\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"eventsBackground\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"leaderboardBackground\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"theme\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Event\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"date\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"type\",\"kind\":\"enum\",\"type\":\"EventType\"},{\"name\":\"status\",\"kind\":\"enum\",\"type\":\"EventStatus\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"SitePreferences\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"homeBackground\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"eventsBackground\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"leaderboardBackground\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
||||||
|
|
||||||
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
async function decodeBase64AsWasm(wasmBase64: string): Promise<WebAssembly.Module> {
|
||||||
const { Buffer } = await import('node:buffer')
|
const { Buffer } = await import('node:buffer')
|
||||||
@@ -203,6 +203,16 @@ export interface PrismaClient<
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
get event(): Prisma.EventDelegate<ExtArgs, { omit: OmitOpts }>;
|
get event(): Prisma.EventDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `prisma.sitePreferences`: Exposes CRUD operations for the **SitePreferences** model.
|
||||||
|
* Example usage:
|
||||||
|
* ```ts
|
||||||
|
* // Fetch zero or more SitePreferences
|
||||||
|
* const sitePreferences = await prisma.sitePreferences.findMany()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
get sitePreferences(): Prisma.SitePreferencesDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPrismaClientClass(): PrismaClientConstructor {
|
export function getPrismaClientClass(): PrismaClientConstructor {
|
||||||
|
|||||||
@@ -386,7 +386,8 @@ type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRe
|
|||||||
export const ModelName = {
|
export const ModelName = {
|
||||||
User: 'User',
|
User: 'User',
|
||||||
UserPreferences: 'UserPreferences',
|
UserPreferences: 'UserPreferences',
|
||||||
Event: 'Event'
|
Event: 'Event',
|
||||||
|
SitePreferences: 'SitePreferences'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
@@ -402,7 +403,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
omit: GlobalOmitOptions
|
omit: GlobalOmitOptions
|
||||||
}
|
}
|
||||||
meta: {
|
meta: {
|
||||||
modelProps: "user" | "userPreferences" | "event"
|
modelProps: "user" | "userPreferences" | "event" | "sitePreferences"
|
||||||
txIsolationLevel: TransactionIsolationLevel
|
txIsolationLevel: TransactionIsolationLevel
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
@@ -628,6 +629,80 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SitePreferences: {
|
||||||
|
payload: Prisma.$SitePreferencesPayload<ExtArgs>
|
||||||
|
fields: Prisma.SitePreferencesFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.SitePreferencesFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$SitePreferencesPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.SitePreferencesFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$SitePreferencesPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.SitePreferencesFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$SitePreferencesPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.SitePreferencesFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$SitePreferencesPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.SitePreferencesFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$SitePreferencesPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.SitePreferencesCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$SitePreferencesPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.SitePreferencesCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
createManyAndReturn: {
|
||||||
|
args: Prisma.SitePreferencesCreateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$SitePreferencesPayload>[]
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.SitePreferencesDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$SitePreferencesPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.SitePreferencesUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$SitePreferencesPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.SitePreferencesDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.SitePreferencesUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateManyAndReturn: {
|
||||||
|
args: Prisma.SitePreferencesUpdateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$SitePreferencesPayload>[]
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.SitePreferencesUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$SitePreferencesPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.SitePreferencesAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateSitePreferences>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.SitePreferencesGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.SitePreferencesGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.SitePreferencesCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.SitePreferencesCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} & {
|
} & {
|
||||||
other: {
|
other: {
|
||||||
@@ -712,6 +787,18 @@ export const EventScalarFieldEnum = {
|
|||||||
export type EventScalarFieldEnum = (typeof EventScalarFieldEnum)[keyof typeof EventScalarFieldEnum]
|
export type EventScalarFieldEnum = (typeof EventScalarFieldEnum)[keyof typeof EventScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const SitePreferencesScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
homeBackground: 'homeBackground',
|
||||||
|
eventsBackground: 'eventsBackground',
|
||||||
|
leaderboardBackground: 'leaderboardBackground',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type SitePreferencesScalarFieldEnum = (typeof SitePreferencesScalarFieldEnum)[keyof typeof SitePreferencesScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -880,6 +967,7 @@ export type GlobalOmitConfig = {
|
|||||||
user?: Prisma.UserOmit
|
user?: Prisma.UserOmit
|
||||||
userPreferences?: Prisma.UserPreferencesOmit
|
userPreferences?: Prisma.UserPreferencesOmit
|
||||||
event?: Prisma.EventOmit
|
event?: Prisma.EventOmit
|
||||||
|
sitePreferences?: Prisma.SitePreferencesOmit
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Types for Logging */
|
/* Types for Logging */
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ export const AnyNull = runtime.AnyNull
|
|||||||
export const ModelName = {
|
export const ModelName = {
|
||||||
User: 'User',
|
User: 'User',
|
||||||
UserPreferences: 'UserPreferences',
|
UserPreferences: 'UserPreferences',
|
||||||
Event: 'Event'
|
Event: 'Event',
|
||||||
|
SitePreferences: 'SitePreferences'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
@@ -117,6 +118,18 @@ export const EventScalarFieldEnum = {
|
|||||||
export type EventScalarFieldEnum = (typeof EventScalarFieldEnum)[keyof typeof EventScalarFieldEnum]
|
export type EventScalarFieldEnum = (typeof EventScalarFieldEnum)[keyof typeof EventScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const SitePreferencesScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
homeBackground: 'homeBackground',
|
||||||
|
eventsBackground: 'eventsBackground',
|
||||||
|
leaderboardBackground: 'leaderboardBackground',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type SitePreferencesScalarFieldEnum = (typeof SitePreferencesScalarFieldEnum)[keyof typeof SitePreferencesScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
|
|||||||
@@ -11,4 +11,5 @@
|
|||||||
export type * from './models/User'
|
export type * from './models/User'
|
||||||
export type * from './models/UserPreferences'
|
export type * from './models/UserPreferences'
|
||||||
export type * from './models/Event'
|
export type * from './models/Event'
|
||||||
|
export type * from './models/SitePreferences'
|
||||||
export type * from './commonInputTypes'
|
export type * from './commonInputTypes'
|
||||||
1170
prisma/generated/prisma/models/SitePreferences.ts
Normal file
1170
prisma/generated/prisma/models/SitePreferences.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "SitePreferences" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY DEFAULT 'global',
|
||||||
|
"homeBackground" TEXT,
|
||||||
|
"eventsBackground" TEXT,
|
||||||
|
"leaderboardBackground" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
@@ -79,3 +79,12 @@ model Event {
|
|||||||
@@index([status])
|
@@index([status])
|
||||||
@@index([date])
|
@@index([date])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model SitePreferences {
|
||||||
|
id String @id @default("global")
|
||||||
|
homeBackground String?
|
||||||
|
eventsBackground String?
|
||||||
|
leaderboardBackground String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|||||||
0
public/uploads/.gitkeep
Normal file
0
public/uploads/.gitkeep
Normal file
BIN
public/uploads/1765265821255-FlashBackground.png
Normal file
BIN
public/uploads/1765265821255-FlashBackground.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 MiB |
Reference in New Issue
Block a user