Compare commits
10 Commits
f62843efcf
...
b1f36f6210
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1f36f6210 | ||
|
|
4e38bd1e8e | ||
|
|
447ef9d076 | ||
|
|
f732eb7385 | ||
|
|
d1e94f1402 | ||
|
|
82c557e10c | ||
|
|
4de3fea776 | ||
|
|
8c326bdd20 | ||
|
|
4486f305f2 | ||
|
|
f57a30eb4d |
10
.env
Normal file
10
.env
Normal file
@@ -0,0 +1,10 @@
|
||||
# Environment variables declared in this file are NOT automatically loaded by Prisma.
|
||||
# Please add `import "dotenv/config";` to your `prisma.config.ts` file, or use the Prisma CLI with Bun
|
||||
# to load environment variables from .env files: https://pris.ly/prisma-config-env-vars.
|
||||
|
||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||
|
||||
DATABASE_URL="file:./dev.db"
|
||||
AUTH_SECRET="your-secret-key-change-this-in-production"
|
||||
AUTH_URL="http://localhost:3000"
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -34,3 +34,10 @@ yarn-error.log*
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# database
|
||||
*.db
|
||||
*.db-journal
|
||||
dev.db*
|
||||
|
||||
# prisma
|
||||
/app/generated/prisma
|
||||
|
||||
328
app/admin/page.tsx
Normal file
328
app/admin/page.tsx
Normal file
@@ -0,0 +1,328 @@
|
||||
"use client";
|
||||
|
||||
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";
|
||||
import UserManagement from "@/components/UserManagement";
|
||||
import EventManagement from "@/components/EventManagement";
|
||||
|
||||
interface SitePreferences {
|
||||
id: string;
|
||||
homeBackground: string | null;
|
||||
eventsBackground: string | null;
|
||||
leaderboardBackground: string | null;
|
||||
}
|
||||
|
||||
type AdminSection = "preferences" | "users" | "events";
|
||||
|
||||
export default function AdminPage() {
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
const [activeSection, setActiveSection] =
|
||||
useState<AdminSection>("preferences");
|
||||
const [preferences, setPreferences] = useState<SitePreferences | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
homeBackground: "",
|
||||
eventsBackground: "",
|
||||
leaderboardBackground: "",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "unauthenticated") {
|
||||
router.push("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === "authenticated" && session?.user?.role !== "ADMIN") {
|
||||
router.push("/");
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === "authenticated" && session?.user?.role === "ADMIN") {
|
||||
fetchPreferences();
|
||||
}
|
||||
}, [status, session, router]);
|
||||
|
||||
const fetchPreferences = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/admin/preferences");
|
||||
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);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/admin/preferences", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await fetchPreferences();
|
||||
setIsEditing(false);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating preferences:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsEditing(false);
|
||||
if (preferences) {
|
||||
setFormData({
|
||||
homeBackground: preferences.homeBackground || "",
|
||||
eventsBackground: preferences.eventsBackground || "",
|
||||
leaderboardBackground: preferences.leaderboardBackground || "",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (status === "loading" || loading) {
|
||||
return (
|
||||
<main className="min-h-screen bg-black relative">
|
||||
<Navigation />
|
||||
<div className="flex items-center justify-center min-h-screen text-pixel-gold">
|
||||
Chargement...
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black relative">
|
||||
<Navigation />
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-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">
|
||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
||||
ADMIN
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{/* 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>
|
||||
<button
|
||||
onClick={() => setActiveSection("events")}
|
||||
className={`px-6 py-3 border uppercase text-xs tracking-widest rounded transition ${
|
||||
activeSection === "events"
|
||||
? "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"
|
||||
}`}
|
||||
>
|
||||
Événements
|
||||
</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">
|
||||
Images de fond du site
|
||||
</h3>
|
||||
<p className="text-gray-400 text-sm">
|
||||
Ces préférences s'appliquent à tous les utilisateurs
|
||||
</p>
|
||||
</div>
|
||||
{!isEditing && (
|
||||
<button
|
||||
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
|
||||
</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 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 items-center gap-4">
|
||||
<span className="text-pixel-gold font-bold min-w-[120px]">
|
||||
Home:
|
||||
</span>
|
||||
{preferences?.homeBackground ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={preferences.homeBackground}
|
||||
alt="Home background"
|
||||
className="w-20 h-12 object-cover rounded border border-pixel-gold/30"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = "/got-2.jpg";
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-gray-400 truncate max-w-xs">
|
||||
{preferences.homeBackground}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400">Par défaut</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-pixel-gold font-bold min-w-[120px]">
|
||||
Events:
|
||||
</span>
|
||||
{preferences?.eventsBackground ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={preferences.eventsBackground}
|
||||
alt="Events background"
|
||||
className="w-20 h-12 object-cover rounded border border-pixel-gold/30"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = "/got-2.jpg";
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-gray-400 truncate max-w-xs">
|
||||
{preferences.eventsBackground}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400">Par défaut</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-pixel-gold font-bold min-w-[120px]">
|
||||
Leaderboard:
|
||||
</span>
|
||||
{preferences?.leaderboardBackground ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<img
|
||||
src={preferences.leaderboardBackground}
|
||||
alt="Leaderboard background"
|
||||
className="w-20 h-12 object-cover rounded border border-pixel-gold/30"
|
||||
onError={(e) => {
|
||||
e.currentTarget.src = "/got-2.jpg";
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-gray-400 truncate max-w-xs">
|
||||
{preferences.leaderboardBackground}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-gray-400">Par défaut</span>
|
||||
)}
|
||||
</div>
|
||||
</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>
|
||||
<UserManagement />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === "events" && (
|
||||
<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 Événements
|
||||
</h2>
|
||||
<EventManagement />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
116
app/api/admin/events/[id]/route.ts
Normal file
116
app/api/admin/events/[id]/route.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { Role, EventType, EventStatus } from "@/prisma/generated/prisma/client";
|
||||
|
||||
export async function PUT(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user || session.user.role !== Role.ADMIN) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { date, name, description, type, status } = body;
|
||||
|
||||
// Vérifier que l'événement existe
|
||||
const existingEvent = await prisma.event.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!existingEvent) {
|
||||
return NextResponse.json(
|
||||
{ error: "Événement non trouvé" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
const updateData: {
|
||||
date?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
type?: EventType;
|
||||
status?: EventStatus;
|
||||
} = {};
|
||||
|
||||
if (date !== undefined) updateData.date = date;
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (description !== undefined) updateData.description = description;
|
||||
if (type !== undefined) {
|
||||
if (!Object.values(EventType).includes(type)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Type d'événement invalide" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
updateData.type = type as EventType;
|
||||
}
|
||||
if (status !== undefined) {
|
||||
if (!Object.values(EventStatus).includes(status)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Statut d'événement invalide" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
updateData.status = status as EventStatus;
|
||||
}
|
||||
|
||||
const event = await prisma.event.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
});
|
||||
|
||||
return NextResponse.json(event);
|
||||
} catch (error) {
|
||||
console.error("Error updating event:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la mise à jour de l'événement" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user || session.user.role !== Role.ADMIN) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
// Vérifier que l'événement existe
|
||||
const existingEvent = await prisma.event.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!existingEvent) {
|
||||
return NextResponse.json(
|
||||
{ error: "Événement non trouvé" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.event.delete({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error deleting event:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la suppression de l'événement" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
82
app/api/admin/events/route.ts
Normal file
82
app/api/admin/events/route.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { Role, EventType, EventStatus } from "@/prisma/generated/prisma/client";
|
||||
|
||||
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 events = await prisma.event.findMany({
|
||||
orderBy: {
|
||||
date: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(events);
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la récupération des événements" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 body = await request.json();
|
||||
const { date, name, description, type, status } = body;
|
||||
|
||||
if (!date || !name || !description || !type || !status) {
|
||||
return NextResponse.json(
|
||||
{ error: "Tous les champs sont requis" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Valider les enums
|
||||
if (!Object.values(EventType).includes(type)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Type d'événement invalide" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!Object.values(EventStatus).includes(status)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Statut d'événement invalide" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const event = await prisma.event.create({
|
||||
data: {
|
||||
date,
|
||||
name,
|
||||
description,
|
||||
type: type as EventType,
|
||||
status: status as EventStatus,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(event);
|
||||
} catch (error) {
|
||||
console.error("Error creating event:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la création de l'événement" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
82
app/api/admin/preferences/route.ts
Normal file
82
app/api/admin/preferences/route.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
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 || session.user.role !== Role.ADMIN) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Récupérer les préférences globales du site
|
||||
let sitePreferences = await prisma.sitePreferences.findUnique({
|
||||
where: { id: "global" },
|
||||
});
|
||||
|
||||
// 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(
|
||||
{ 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 || session.user.role !== Role.ADMIN) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { homeBackground, eventsBackground, leaderboardBackground } = body;
|
||||
|
||||
const preferences = await prisma.sitePreferences.upsert({
|
||||
where: { id: "global" },
|
||||
update: {
|
||||
homeBackground:
|
||||
homeBackground === "" ? null : homeBackground ?? undefined,
|
||||
eventsBackground:
|
||||
eventsBackground === "" ? null : eventsBackground ?? undefined,
|
||||
leaderboardBackground:
|
||||
leaderboardBackground === ""
|
||||
? null
|
||||
: leaderboardBackground ?? undefined,
|
||||
},
|
||||
create: {
|
||||
id: "global",
|
||||
homeBackground: homeBackground === "" ? null : homeBackground ?? null,
|
||||
eventsBackground:
|
||||
eventsBackground === "" ? null : eventsBackground ?? null,
|
||||
leaderboardBackground:
|
||||
leaderboardBackground === "" ? null : leaderboardBackground ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(preferences);
|
||||
} catch (error) {
|
||||
console.error("Error updating admin preferences:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la mise à jour des préférences" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
151
app/api/admin/users/[id]/route.ts
Normal file
151
app/api/admin/users/[id]/route.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
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 PUT(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user || session.user.role !== Role.ADMIN) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { hpDelta, xpDelta, score, level, role } = body;
|
||||
|
||||
// Récupérer l'utilisateur actuel
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: "Utilisateur non trouvé" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Calculer les nouvelles valeurs
|
||||
let newHp = user.hp;
|
||||
let newXp = user.xp;
|
||||
let newLevel = user.level;
|
||||
let newMaxXp = user.maxXp;
|
||||
|
||||
// Appliquer les changements de HP
|
||||
if (hpDelta !== undefined) {
|
||||
newHp = Math.max(0, Math.min(user.maxHp, user.hp + hpDelta));
|
||||
}
|
||||
|
||||
// Appliquer les changements de XP
|
||||
if (xpDelta !== undefined) {
|
||||
newXp = user.xp + xpDelta;
|
||||
newLevel = user.level;
|
||||
newMaxXp = user.maxXp;
|
||||
|
||||
// Gérer le niveau up si nécessaire (quand on ajoute de l'XP)
|
||||
if (newXp >= newMaxXp && newXp > 0) {
|
||||
while (newXp >= newMaxXp) {
|
||||
newXp -= newMaxXp;
|
||||
newLevel += 1;
|
||||
// Augmenter le maxXp pour le prochain niveau (formule simple)
|
||||
newMaxXp = Math.floor(newMaxXp * 1.2);
|
||||
}
|
||||
}
|
||||
|
||||
// Gérer le niveau down si nécessaire (quand on enlève de l'XP)
|
||||
if (newXp < 0 && newLevel > 1) {
|
||||
while (newXp < 0 && newLevel > 1) {
|
||||
newLevel -= 1;
|
||||
// Calculer le maxXp du niveau précédent
|
||||
newMaxXp = Math.floor(newMaxXp / 1.2);
|
||||
newXp += newMaxXp;
|
||||
}
|
||||
// S'assurer que l'XP ne peut pas être négative
|
||||
newXp = Math.max(0, newXp);
|
||||
}
|
||||
|
||||
// S'assurer que le niveau minimum est 1
|
||||
if (newLevel < 1) {
|
||||
newLevel = 1;
|
||||
newXp = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Appliquer les changements directs (score, level, role)
|
||||
const updateData: {
|
||||
hp: number;
|
||||
xp: number;
|
||||
level: number;
|
||||
maxXp: number;
|
||||
score?: number;
|
||||
role?: Role;
|
||||
} = {
|
||||
hp: newHp,
|
||||
xp: newXp,
|
||||
level: newLevel,
|
||||
maxXp: newMaxXp,
|
||||
};
|
||||
|
||||
if (score !== undefined) {
|
||||
updateData.score = Math.max(0, score);
|
||||
}
|
||||
|
||||
if (level !== undefined) {
|
||||
// Si le niveau est modifié directement, utiliser cette valeur
|
||||
const targetLevel = Math.max(1, level);
|
||||
updateData.level = targetLevel;
|
||||
|
||||
// Recalculer le maxXp pour le nouveau niveau
|
||||
// Formule: maxXp = 5000 * (1.2 ^ (level - 1))
|
||||
let calculatedMaxXp = 5000;
|
||||
for (let i = 1; i < targetLevel; i++) {
|
||||
calculatedMaxXp = Math.floor(calculatedMaxXp * 1.2);
|
||||
}
|
||||
updateData.maxXp = calculatedMaxXp;
|
||||
|
||||
// Réinitialiser l'XP si le niveau change directement (sauf si on modifie aussi l'XP)
|
||||
if (targetLevel !== user.level && xpDelta === undefined) {
|
||||
updateData.xp = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (role !== undefined) {
|
||||
if (role === "ADMIN" || role === "USER") {
|
||||
updateData.role = role as Role;
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour l'utilisateur
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id },
|
||||
data: updateData,
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
role: true,
|
||||
score: true,
|
||||
level: true,
|
||||
hp: true,
|
||||
maxHp: true,
|
||||
xp: true,
|
||||
maxXp: true,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updatedUser);
|
||||
} catch (error) {
|
||||
console.error("Error updating user:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la mise à jour de l'utilisateur" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
44
app/api/admin/users/route.ts
Normal file
44
app/api/admin/users/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
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 || session.user.role !== Role.ADMIN) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Récupérer tous les utilisateurs avec leurs stats
|
||||
const users = await prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
role: true,
|
||||
score: true,
|
||||
level: true,
|
||||
hp: true,
|
||||
maxHp: true,
|
||||
xp: true,
|
||||
maxXp: true,
|
||||
avatar: true,
|
||||
createdAt: true,
|
||||
},
|
||||
orderBy: {
|
||||
score: "desc",
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(users);
|
||||
} catch (error) {
|
||||
console.error("Error fetching users:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la récupération des utilisateurs" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
4
app/api/auth/[...nextauth]/route.ts
Normal file
4
app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { handlers } from "@/lib/auth";
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
|
||||
21
app/api/events/route.ts
Normal file
21
app/api/events/route.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const events = await prisma.event.findMany({
|
||||
orderBy: {
|
||||
date: "asc",
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(events);
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la récupération des événements" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
37
app/api/leaderboard/route.ts
Normal file
37
app/api/leaderboard/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const users = await prisma.user.findMany({
|
||||
orderBy: {
|
||||
score: "desc",
|
||||
},
|
||||
take: 10,
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
score: true,
|
||||
level: true,
|
||||
avatar: true,
|
||||
},
|
||||
});
|
||||
|
||||
const leaderboard = users.map((user: { id: string; username: string; score: number; level: number; avatar: string | null }, index: number) => ({
|
||||
rank: index + 1,
|
||||
username: user.username,
|
||||
score: user.score,
|
||||
level: user.level,
|
||||
avatar: user.avatar,
|
||||
}));
|
||||
|
||||
return NextResponse.json(leaderboard);
|
||||
} catch (error) {
|
||||
console.error("Error fetching leaderboard:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la récupération du leaderboard" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
36
app/api/preferences/route.ts
Normal file
36
app/api/preferences/route.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
// Récupérer les préférences globales du site (pas besoin d'authentification)
|
||||
let sitePreferences = await prisma.sitePreferences.findUnique({
|
||||
where: { id: "global" },
|
||||
});
|
||||
|
||||
// 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(
|
||||
{
|
||||
homeBackground: null,
|
||||
eventsBackground: null,
|
||||
leaderboardBackground: null,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
}
|
||||
}
|
||||
66
app/api/profile/avatar/route.ts
Normal file
66
app/api/profile/avatar/route.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
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) {
|
||||
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
||||
}
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
|
||||
// Limiter la taille (par exemple 5MB)
|
||||
const maxSize = 5 * 1024 * 1024; // 5MB
|
||||
if (file.size > maxSize) {
|
||||
return NextResponse.json(
|
||||
{ error: "L'image est trop grande (max 5MB)" },
|
||||
{ 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 avec l'ID utilisateur
|
||||
const timestamp = Date.now();
|
||||
const filename = `avatar-${session.user.id}-${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 avatar:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de l'upload de l'avatar" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
80
app/api/profile/password/route.ts
Normal file
80
app/api/profile/password/route.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
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 { currentPassword, newPassword, confirmPassword } = body;
|
||||
|
||||
// Validation
|
||||
if (!currentPassword || !newPassword || !confirmPassword) {
|
||||
return NextResponse.json(
|
||||
{ error: "Tous les champs sont requis" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
return NextResponse.json(
|
||||
{ error: "Le nouveau mot de passe doit contenir au moins 6 caractères" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
return NextResponse.json(
|
||||
{ error: "Les mots de passe ne correspondent pas" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Récupérer l'utilisateur avec le mot de passe
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: { password: true },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json(
|
||||
{ error: "Utilisateur non trouvé" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Vérifier l'ancien mot de passe
|
||||
const isPasswordValid = await bcrypt.compare(currentPassword, user.password);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return NextResponse.json(
|
||||
{ error: "Mot de passe actuel incorrect" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hasher le nouveau mot de passe
|
||||
const hashedPassword = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
// Mettre à jour le mot de passe
|
||||
await prisma.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: { password: hashedPassword },
|
||||
});
|
||||
|
||||
return NextResponse.json({ message: "Mot de passe modifié avec succès" });
|
||||
} catch (error) {
|
||||
console.error("Error updating password:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la modification du mot de passe" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
122
app/api/profile/route.ts
Normal file
122
app/api/profile/route.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
hp: true,
|
||||
maxHp: true,
|
||||
xp: true,
|
||||
maxXp: true,
|
||||
level: true,
|
||||
score: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Utilisateur non trouvé" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(user);
|
||||
} catch (error) {
|
||||
console.error("Error fetching profile:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la récupération du profil" },
|
||||
{ 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 { username, avatar } = body;
|
||||
|
||||
// Validation
|
||||
if (username !== undefined) {
|
||||
if (typeof username !== "string" || username.trim().length === 0) {
|
||||
return NextResponse.json(
|
||||
{ error: "Le nom d'utilisateur ne peut pas être vide" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (username.length < 3 || username.length > 20) {
|
||||
return NextResponse.json(
|
||||
{ error: "Le nom d'utilisateur doit contenir entre 3 et 20 caractères" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Vérifier si le username est déjà pris par un autre utilisateur
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
username: username.trim(),
|
||||
NOT: { id: session.user.id },
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ error: "Ce nom d'utilisateur est déjà pris" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour l'utilisateur
|
||||
const updateData: { username?: string; avatar?: string | null } = {};
|
||||
if (username !== undefined) {
|
||||
updateData.username = username.trim();
|
||||
}
|
||||
if (avatar !== undefined) {
|
||||
updateData.avatar = avatar || null;
|
||||
}
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: updateData,
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
hp: true,
|
||||
maxHp: true,
|
||||
xp: true,
|
||||
maxXp: true,
|
||||
level: true,
|
||||
score: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updatedUser);
|
||||
} catch (error) {
|
||||
console.error("Error updating profile:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la mise à jour du profil" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
62
app/api/register/route.ts
Normal file
62
app/api/register/route.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { email, username, password } = body;
|
||||
|
||||
if (!email || !username || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: "Tous les champs sont requis" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
return NextResponse.json(
|
||||
{ error: "Le mot de passe doit contenir au moins 6 caractères" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Vérifier si l'email existe déjà
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
OR: [{ email }, { username }],
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return NextResponse.json(
|
||||
{ error: "Cet email ou nom d'utilisateur est déjà utilisé" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hasher le mot de passe
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Créer l'utilisateur
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
username,
|
||||
password: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(
|
||||
{ message: "Compte créé avec succès", userId: user.id },
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Registration error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Une erreur est survenue lors de l'inscription" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
50
app/api/users/[id]/route.ts
Normal file
50
app/api/users/[id]/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET(
|
||||
request: Request,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await auth();
|
||||
const { id } = await params;
|
||||
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
|
||||
}
|
||||
|
||||
// Vérifier que l'utilisateur demande ses propres données ou est admin
|
||||
if (session.user.id !== id && session.user.role !== "ADMIN") {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
hp: true,
|
||||
maxHp: true,
|
||||
xp: true,
|
||||
maxXp: true,
|
||||
level: true,
|
||||
score: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: "Utilisateur non trouvé" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(user);
|
||||
} catch (error) {
|
||||
console.error("Error fetching user:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la récupération de l'utilisateur" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Orbitron, Rajdhani } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import SessionProvider from "@/components/SessionProvider";
|
||||
|
||||
const orbitron = Orbitron({
|
||||
subsets: ["latin"],
|
||||
@@ -26,8 +27,9 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="fr" className={`${orbitron.variable} ${rajdhani.variable}`}>
|
||||
<body className="antialiased">{children}</body>
|
||||
<body className="antialiased">
|
||||
<SessionProvider>{children}</SessionProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
141
app/login/page.tsx
Normal file
141
app/login/page.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Navigation from "@/components/Navigation";
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const result = await signIn("credentials", {
|
||||
email,
|
||||
password,
|
||||
redirect: false,
|
||||
callbackUrl: "/",
|
||||
});
|
||||
|
||||
if (result?.error) {
|
||||
setError("Email ou mot de passe incorrect");
|
||||
setLoading(false);
|
||||
} else if (result?.ok) {
|
||||
router.push("/");
|
||||
router.refresh();
|
||||
} else {
|
||||
setError("Une erreur est survenue lors de la connexion");
|
||||
setLoading(false);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Login error:", err);
|
||||
setError("Une erreur est survenue");
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black relative">
|
||||
<Navigation />
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('/got-2.jpg')`,
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||||
</div>
|
||||
|
||||
{/* Login Form */}
|
||||
<div className="relative z-10 w-full max-w-md mx-auto px-8">
|
||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm">
|
||||
<h1 className="text-4xl font-gaming font-black mb-2 text-center">
|
||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
||||
CONNEXION
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-gray-400 text-sm text-center mb-8">
|
||||
Connectez-vous à votre compte
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{error && (
|
||||
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
||||
placeholder="votre@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full px-6 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "Connexion..." : "Se connecter"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-gray-400 text-sm">
|
||||
Pas encore de compte ?{" "}
|
||||
<Link
|
||||
href="/register"
|
||||
className="text-pixel-gold hover:text-orange-400 transition"
|
||||
>
|
||||
S'inscrire
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
499
app/profile/page.tsx
Normal file
499
app/profile/page.tsx
Normal file
@@ -0,0 +1,499 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Navigation from "@/components/Navigation";
|
||||
import { useBackgroundImage } from "@/hooks/usePreferences";
|
||||
|
||||
interface UserProfile {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
hp: number;
|
||||
maxHp: number;
|
||||
xp: number;
|
||||
maxXp: number;
|
||||
level: number;
|
||||
score: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
return num.toLocaleString("en-US");
|
||||
};
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { data: session, status } = useSession();
|
||||
const router = useRouter();
|
||||
const backgroundImage = useBackgroundImage("home", "/got-background.jpg");
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
const [avatar, setAvatar] = useState<string | null>(null);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [uploadingAvatar, setUploadingAvatar] = useState(false);
|
||||
|
||||
// Password change form state
|
||||
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [changingPassword, setChangingPassword] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "unauthenticated") {
|
||||
router.push("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
if (status === "authenticated" && session?.user) {
|
||||
fetchProfile();
|
||||
}
|
||||
}, [status, session, router]);
|
||||
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/profile");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setProfile(data);
|
||||
setUsername(data.username);
|
||||
setAvatar(data.avatar);
|
||||
} else {
|
||||
setError("Erreur lors du chargement du profil");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error fetching profile:", err);
|
||||
setError("Erreur lors du chargement du profil");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setUploadingAvatar(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const response = await fetch("/api/profile/avatar", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAvatar(data.url);
|
||||
setSuccess("Avatar mis à jour avec succès");
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setError(errorData.error || "Erreur lors de l'upload de l'avatar");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error uploading avatar:", err);
|
||||
setError("Erreur lors de l'upload de l'avatar");
|
||||
} finally {
|
||||
setUploadingAvatar(false);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/profile", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
avatar,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setProfile(data);
|
||||
setSuccess("Profil mis à jour avec succès");
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setError(errorData.error || "Erreur lors de la mise à jour");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error updating profile:", err);
|
||||
setError("Erreur lors de la mise à jour du profil");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordChange = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setChangingPassword(true);
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/profile/password", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
currentPassword,
|
||||
newPassword,
|
||||
confirmPassword,
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setSuccess("Mot de passe modifié avec succès");
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
setConfirmPassword("");
|
||||
setShowPasswordForm(false);
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setError(errorData.error || "Erreur lors de la modification du mot de passe");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error changing password:", err);
|
||||
setError("Erreur lors de la modification du mot de passe");
|
||||
} finally {
|
||||
setChangingPassword(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading || status === "loading") {
|
||||
return (
|
||||
<main className="min-h-screen bg-black relative">
|
||||
<Navigation />
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
|
||||
<div className="text-pixel-gold text-xl">Chargement...</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
if (!profile) {
|
||||
return (
|
||||
<main className="min-h-screen bg-black relative">
|
||||
<Navigation />
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
|
||||
<div className="text-red-400 text-xl">Erreur lors du chargement du profil</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const hpPercentage = (profile.hp / profile.maxHp) * 100;
|
||||
const xpPercentage = (profile.xp / profile.maxXp) * 100;
|
||||
|
||||
const hpColor =
|
||||
hpPercentage > 60
|
||||
? "from-green-600 to-green-700"
|
||||
: hpPercentage > 30
|
||||
? "from-yellow-600 to-orange-700"
|
||||
: "from-red-700 to-red-900";
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black relative">
|
||||
<Navigation />
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('${backgroundImage}')`,
|
||||
}}
|
||||
>
|
||||
{/* Dark overlay for readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 w-full max-w-4xl mx-auto px-8 py-16">
|
||||
{/* Title Section */}
|
||||
<div className="text-center mb-12">
|
||||
<h1 className="text-5xl md:text-7xl font-gaming font-black mb-4 tracking-tight">
|
||||
<span
|
||||
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
|
||||
style={{
|
||||
textShadow: "0 0 30px rgba(218, 165, 32, 0.5)",
|
||||
}}
|
||||
>
|
||||
PROFIL
|
||||
</span>
|
||||
</h1>
|
||||
<div className="text-pixel-gold text-lg md:text-xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 tracking-wide">
|
||||
<span>✦</span>
|
||||
<span>Gérez votre profil</span>
|
||||
<span>✦</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Profile Card */}
|
||||
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg overflow-hidden backdrop-blur-sm">
|
||||
<form onSubmit={handleSubmit} className="p-8 space-y-8">
|
||||
{/* Messages */}
|
||||
{error && (
|
||||
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{success && (
|
||||
<div className="bg-green-900/50 border border-green-500/50 text-green-400 px-4 py-3 rounded text-sm">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Avatar Section */}
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="relative">
|
||||
<div className="w-32 h-32 rounded-full border-4 border-pixel-gold/50 overflow-hidden bg-gray-900 flex items-center justify-center">
|
||||
{avatar ? (
|
||||
<img
|
||||
src={avatar}
|
||||
alt={username}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-pixel-gold text-4xl font-bold">
|
||||
{username.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{uploadingAvatar && (
|
||||
<div className="absolute inset-0 bg-black/60 flex items-center justify-center rounded-full">
|
||||
<div className="text-pixel-gold text-sm">Upload...</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarUpload}
|
||||
className="hidden"
|
||||
id="avatar-upload"
|
||||
/>
|
||||
<label
|
||||
htmlFor="avatar-upload"
|
||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition cursor-pointer inline-block"
|
||||
>
|
||||
{uploadingAvatar ? "Upload en cours..." : "Changer l'avatar"}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Username Field */}
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
||||
Nom d'utilisateur
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={20}
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
3-20 caractères
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Display */}
|
||||
<div className="border-t border-pixel-gold/20 pt-6">
|
||||
<h3 className="text-pixel-gold text-sm uppercase tracking-widest mb-4">
|
||||
Statistiques
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="bg-black/40 border border-pixel-gold/20 rounded p-4">
|
||||
<div className="text-gray-400 text-xs uppercase mb-1">Score</div>
|
||||
<div className="text-pixel-gold text-xl font-bold">
|
||||
{formatNumber(profile.score)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-black/40 border border-pixel-gold/20 rounded p-4">
|
||||
<div className="text-gray-400 text-xs uppercase mb-1">Niveau</div>
|
||||
<div className="text-pixel-gold text-xl font-bold">
|
||||
Lv.{profile.level}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* HP Bar */}
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
||||
<span>HP</span>
|
||||
<span>{profile.hp} / {profile.maxHp}</span>
|
||||
</div>
|
||||
<div className="relative h-3 bg-gray-900 border border-gray-700 rounded overflow-hidden">
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-r ${hpColor} transition-all duration-1000 ease-out`}
|
||||
style={{ width: `${hpPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* XP Bar */}
|
||||
<div>
|
||||
<div className="flex justify-between text-xs text-gray-400 mb-1">
|
||||
<span>XP</span>
|
||||
<span>{formatNumber(profile.xp)} / {formatNumber(profile.maxXp)}</span>
|
||||
</div>
|
||||
<div className="relative h-3 bg-gray-900 border border-pixel-gold/30 rounded overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80 transition-all duration-1000 ease-out"
|
||||
style={{ width: `${xpPercentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Email (read-only) */}
|
||||
<div>
|
||||
<label className="block text-gray-500 text-sm uppercase tracking-widest mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={profile.email}
|
||||
disabled
|
||||
className="w-full px-4 py-3 bg-black/20 border border-gray-700/50 rounded text-gray-500 cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex justify-end gap-4 pt-4 border-t border-pixel-gold/20">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="px-6 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{saving ? "Enregistrement..." : "Enregistrer les modifications"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Password Change Section - Separate form */}
|
||||
<div className="border-t border-pixel-gold/20 p-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-pixel-gold text-sm uppercase tracking-widest">
|
||||
Mot de passe
|
||||
</h3>
|
||||
{!showPasswordForm && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPasswordForm(true)}
|
||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition"
|
||||
>
|
||||
Changer le mot de passe
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showPasswordForm && (
|
||||
<form onSubmit={handlePasswordChange} className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
||||
Mot de passe actuel
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
||||
Nouveau mot de passe
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
||||
required
|
||||
minLength={6}
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
Minimum 6 caractères
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-2">
|
||||
Confirmer le nouveau mot de passe
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="w-full px-4 py-3 bg-black/40 border border-pixel-gold/30 rounded text-white focus:outline-none focus:border-pixel-gold transition"
|
||||
required
|
||||
minLength={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShowPasswordForm(false);
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
setConfirmPassword("");
|
||||
setError(null);
|
||||
}}
|
||||
className="px-4 py-2 border border-gray-600/50 bg-black/40 text-gray-400 uppercase text-xs tracking-widest rounded hover:bg-gray-900/40 hover:border-gray-500 transition"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={changingPassword}
|
||||
className="px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{changingPassword ? "Modification..." : "Modifier le mot de passe"}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
204
app/register/page.tsx
Normal file
204
app/register/page.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Navigation from "@/components/Navigation";
|
||||
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter();
|
||||
const [formData, setFormData] = useState({
|
||||
email: "",
|
||||
username: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
const [error, setError] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
[e.target.name]: e.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError("");
|
||||
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
setError("Les mots de passe ne correspondent pas");
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.password.length < 6) {
|
||||
setError("Le mot de passe doit contenir au moins 6 caractères");
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/register", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: formData.email,
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(data.error || "Une erreur est survenue");
|
||||
return;
|
||||
}
|
||||
|
||||
router.push("/login?registered=true");
|
||||
} catch (err) {
|
||||
setError("Une erreur est survenue");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black relative">
|
||||
<Navigation />
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('/got-2.jpg')`,
|
||||
}}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||||
</div>
|
||||
|
||||
{/* Register Form */}
|
||||
<div className="relative z-10 w-full max-w-md mx-auto px-8">
|
||||
<div className="bg-black/80 border border-pixel-gold/30 rounded-lg p-8 backdrop-blur-sm">
|
||||
<h1 className="text-4xl font-gaming font-black mb-2 text-center">
|
||||
<span className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent">
|
||||
INSCRIPTION
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-gray-400 text-sm text-center mb-8">
|
||||
Créez votre compte pour commencer
|
||||
</p>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
{error && (
|
||||
<div className="bg-red-900/50 border border-red-500/50 text-red-400 px-4 py-3 rounded text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
||||
placeholder="votre@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="username"
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
Nom d'utilisateur
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
value={formData.username}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
||||
placeholder="VotrePseudo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
Mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="confirmPassword"
|
||||
className="block text-sm font-semibold text-gray-300 mb-2 uppercase tracking-wider"
|
||||
>
|
||||
Confirmer le mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleChange}
|
||||
required
|
||||
className="w-full px-4 py-3 bg-black/60 border border-pixel-gold/30 rounded text-white placeholder-gray-500 focus:outline-none focus:border-pixel-gold transition"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full px-6 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? "Inscription..." : "S'inscrire"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<p className="text-gray-400 text-sm">
|
||||
Déjà un compte ?{" "}
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-pixel-gold hover:text-orange-400 transition"
|
||||
>
|
||||
Se connecter
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
381
components/EventManagement.tsx
Normal file
381
components/EventManagement.tsx
Normal file
@@ -0,0 +1,381 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface Event {
|
||||
id: string;
|
||||
date: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: "SUMMIT" | "LAUNCH" | "FESTIVAL" | "COMPETITION";
|
||||
status: "UPCOMING" | "LIVE" | "PAST";
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
interface EventFormData {
|
||||
date: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: "SUMMIT" | "LAUNCH" | "FESTIVAL" | "COMPETITION";
|
||||
status: "UPCOMING" | "LIVE" | "PAST";
|
||||
}
|
||||
|
||||
const eventTypes: Event["type"][] = [
|
||||
"SUMMIT",
|
||||
"LAUNCH",
|
||||
"FESTIVAL",
|
||||
"COMPETITION",
|
||||
];
|
||||
const eventStatuses: Event["status"][] = ["UPCOMING", "LIVE", "PAST"];
|
||||
|
||||
const getEventTypeLabel = (type: Event["type"]) => {
|
||||
switch (type) {
|
||||
case "SUMMIT":
|
||||
return "Sommet";
|
||||
case "LAUNCH":
|
||||
return "Lancement";
|
||||
case "FESTIVAL":
|
||||
return "Festival";
|
||||
case "COMPETITION":
|
||||
return "Compétition";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: Event["status"]) => {
|
||||
switch (status) {
|
||||
case "UPCOMING":
|
||||
return "À venir";
|
||||
case "LIVE":
|
||||
return "En cours";
|
||||
case "PAST":
|
||||
return "Passé";
|
||||
default:
|
||||
return status;
|
||||
}
|
||||
};
|
||||
|
||||
export default function EventManagement() {
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [formData, setFormData] = useState<EventFormData>({
|
||||
date: "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: "SUMMIT",
|
||||
status: "UPCOMING",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchEvents();
|
||||
}, []);
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/admin/events");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setEvents(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
setIsCreating(true);
|
||||
setEditingEvent(null);
|
||||
setFormData({
|
||||
date: "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: "SUMMIT",
|
||||
status: "UPCOMING",
|
||||
});
|
||||
};
|
||||
|
||||
const handleEdit = (event: Event) => {
|
||||
setEditingEvent(event);
|
||||
setIsCreating(false);
|
||||
setFormData({
|
||||
date: event.date,
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
type: event.type,
|
||||
status: event.status,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaving(true);
|
||||
try {
|
||||
let response;
|
||||
if (isCreating) {
|
||||
response = await fetch("/api/admin/events", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
} else if (editingEvent) {
|
||||
response = await fetch(`/api/admin/events/${editingEvent.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
}
|
||||
|
||||
if (response?.ok) {
|
||||
await fetchEvents();
|
||||
setEditingEvent(null);
|
||||
setIsCreating(false);
|
||||
setFormData({
|
||||
date: "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: "SUMMIT",
|
||||
status: "UPCOMING",
|
||||
});
|
||||
} else {
|
||||
const error = await response?.json();
|
||||
alert(error.error || "Erreur lors de la sauvegarde");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error saving event:", error);
|
||||
alert("Erreur lors de la sauvegarde");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (eventId: string) => {
|
||||
if (!confirm("Êtes-vous sûr de vouloir supprimer cet événement ?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/events/${eventId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await fetchEvents();
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || "Erreur lors de la suppression");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting event:", error);
|
||||
alert("Erreur lors de la suppression");
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditingEvent(null);
|
||||
setIsCreating(false);
|
||||
setFormData({
|
||||
date: "",
|
||||
name: "",
|
||||
description: "",
|
||||
type: "SUMMIT",
|
||||
status: "UPCOMING",
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-xl font-gaming font-bold text-pixel-gold">
|
||||
Événements ({events.length})
|
||||
</h3>
|
||||
{!isCreating && !editingEvent && (
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
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"
|
||||
>
|
||||
+ Nouvel événement
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{(isCreating || editingEvent) && (
|
||||
<div className="bg-black/60 border border-pixel-gold/20 rounded p-4 mb-4">
|
||||
<h4 className="text-pixel-gold font-bold mb-4">
|
||||
{isCreating ? "Créer un événement" : "Modifier l'événement"}
|
||||
</h4>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-1">Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={formData.date}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, date: e.target.value })
|
||||
}
|
||||
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">Nom</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, name: e.target.value })
|
||||
}
|
||||
placeholder="Nom de l'événement"
|
||||
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">
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, description: e.target.value })
|
||||
}
|
||||
placeholder="Description de l'événement"
|
||||
rows={4}
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-1">Type</label>
|
||||
<select
|
||||
value={formData.type}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
type: e.target.value as Event["type"],
|
||||
})
|
||||
}
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm"
|
||||
>
|
||||
{eventTypes.map((type) => (
|
||||
<option key={type} value={type}>
|
||||
{getEventTypeLabel(type)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-1">
|
||||
Statut
|
||||
</label>
|
||||
<select
|
||||
value={formData.status}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
status: e.target.value as Event["status"],
|
||||
})
|
||||
}
|
||||
className="w-full px-3 py-2 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm"
|
||||
>
|
||||
{eventStatuses.map((status) => (
|
||||
<option key={status} value={status}>
|
||||
{getStatusLabel(status)}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
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 disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Enregistrement..." : "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>
|
||||
)}
|
||||
|
||||
{events.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-8">
|
||||
Aucun événement trouvé
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{events.map((event) => (
|
||||
<div
|
||||
key={event.id}
|
||||
className="bg-black/60 border border-pixel-gold/20 rounded p-4"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h4 className="text-pixel-gold font-bold text-lg">
|
||||
{event.name}
|
||||
</h4>
|
||||
<span className="px-2 py-1 bg-pixel-gold/20 border border-pixel-gold/50 text-pixel-gold text-xs uppercase rounded">
|
||||
{getEventTypeLabel(event.type)}
|
||||
</span>
|
||||
<span
|
||||
className={`px-2 py-1 text-xs uppercase rounded ${
|
||||
event.status === "UPCOMING"
|
||||
? "bg-green-900/50 border border-green-500/50 text-green-400"
|
||||
: event.status === "LIVE"
|
||||
? "bg-yellow-900/50 border border-yellow-500/50 text-yellow-400"
|
||||
: "bg-gray-900/50 border border-gray-500/50 text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{getStatusLabel(event.status)}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-sm mb-2">
|
||||
{event.description}
|
||||
</p>
|
||||
<p className="text-gray-500 text-xs">
|
||||
Date: {new Date(event.date).toLocaleDateString("fr-FR")}
|
||||
</p>
|
||||
</div>
|
||||
{!isCreating && !editingEvent && (
|
||||
<div className="flex gap-2 ml-4">
|
||||
<button
|
||||
onClick={() => handleEdit(event)}
|
||||
className="px-3 py-1 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDelete(event.id)}
|
||||
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-xs tracking-widest rounded hover:bg-red-900/30 transition"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,80 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useBackgroundImage } from "@/hooks/usePreferences";
|
||||
|
||||
interface Event {
|
||||
id: number;
|
||||
id: string;
|
||||
date: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: "summit" | "launch" | "festival" | "competition";
|
||||
status: "upcoming" | "live" | "past";
|
||||
type: "SUMMIT" | "LAUNCH" | "FESTIVAL" | "COMPETITION";
|
||||
status: "UPCOMING" | "LIVE" | "PAST";
|
||||
}
|
||||
|
||||
const events: Event[] = [
|
||||
{
|
||||
id: 1,
|
||||
date: "18 NOVEMBRE 2023",
|
||||
name: "Sommet de l'Innovation Tech",
|
||||
description:
|
||||
"Rejoignez les leaders de l'industrie et les innovateurs pour une journée de discussions sur les technologies de pointe, les percées de l'IA et des opportunités de networking.",
|
||||
type: "summit",
|
||||
status: "past",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: "3 DÉCEMBRE 2023",
|
||||
name: "Lancement de la Révolution IA",
|
||||
description:
|
||||
"Assistez au lancement de systèmes d'IA révolutionnaires qui vont remodeler le paysage du gaming. Aperçus exclusifs et opportunités d'accès anticipé.",
|
||||
type: "launch",
|
||||
status: "past",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
date: "22 DÉCEMBRE 2023",
|
||||
name: "Festival du Code d'Hiver",
|
||||
description:
|
||||
"Une célébration de l'excellence en programmation avec des hackathons, des défis de codage et des prix. Montrez vos compétences et rivalisez avec les meilleurs développeurs.",
|
||||
type: "festival",
|
||||
status: "past",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
date: "15 JANVIER 2024",
|
||||
name: "Expo Informatique Quantique",
|
||||
description:
|
||||
"Explorez l'avenir de l'informatique quantique dans le gaming. Démonstrations interactives, conférences d'experts et ateliers pratiques pour tous les niveaux.",
|
||||
type: "summit",
|
||||
status: "upcoming",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
date: "8 FÉVRIER 2024",
|
||||
name: "Championnat Cyber Arena",
|
||||
description:
|
||||
"L'événement de gaming compétitif ultime. Compétissez pour la gloire, des récompenses exclusives et le titre de Champion Cyber Arena. Inscriptions ouvertes.",
|
||||
type: "competition",
|
||||
status: "upcoming",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
date: "12 MARS 2024",
|
||||
name: "Gala Tech du Printemps",
|
||||
description:
|
||||
"Une soirée élégante célébrant les réalisations technologiques. Cérémonie de remise de prix, networking et annonces exclusives des plus grandes entreprises tech.",
|
||||
type: "festival",
|
||||
status: "upcoming",
|
||||
},
|
||||
];
|
||||
|
||||
const getEventTypeColor = (type: Event["type"]) => {
|
||||
switch (type) {
|
||||
case "summit":
|
||||
case "SUMMIT":
|
||||
return "from-blue-600 to-cyan-500";
|
||||
case "launch":
|
||||
case "LAUNCH":
|
||||
return "from-purple-600 to-pink-500";
|
||||
case "festival":
|
||||
case "FESTIVAL":
|
||||
return "from-pixel-gold to-orange-500";
|
||||
case "competition":
|
||||
case "COMPETITION":
|
||||
return "from-red-600 to-orange-500";
|
||||
default:
|
||||
return "from-gray-600 to-gray-500";
|
||||
@@ -83,13 +29,13 @@ const getEventTypeColor = (type: Event["type"]) => {
|
||||
|
||||
const getEventTypeLabel = (type: Event["type"]) => {
|
||||
switch (type) {
|
||||
case "summit":
|
||||
case "SUMMIT":
|
||||
return "Sommet";
|
||||
case "launch":
|
||||
case "LAUNCH":
|
||||
return "Lancement";
|
||||
case "festival":
|
||||
case "FESTIVAL":
|
||||
return "Festival";
|
||||
case "competition":
|
||||
case "COMPETITION":
|
||||
return "Compétition";
|
||||
default:
|
||||
return type;
|
||||
@@ -98,19 +44,19 @@ const getEventTypeLabel = (type: Event["type"]) => {
|
||||
|
||||
const getStatusBadge = (status: Event["status"]) => {
|
||||
switch (status) {
|
||||
case "upcoming":
|
||||
case "UPCOMING":
|
||||
return (
|
||||
<span className="px-3 py-1 bg-green-900/50 border border-green-500/50 text-green-400 text-xs uppercase tracking-widest rounded">
|
||||
À venir
|
||||
</span>
|
||||
);
|
||||
case "live":
|
||||
case "LIVE":
|
||||
return (
|
||||
<span className="px-3 py-1 bg-red-900/50 border border-red-500/50 text-red-400 text-xs uppercase tracking-widest rounded animate-pulse">
|
||||
En direct
|
||||
</span>
|
||||
);
|
||||
case "past":
|
||||
case "PAST":
|
||||
return (
|
||||
<span className="px-3 py-1 bg-gray-800/50 border border-gray-600/50 text-gray-400 text-xs uppercase tracking-widest rounded">
|
||||
Passé
|
||||
@@ -120,13 +66,37 @@ const getStatusBadge = (status: Event["status"]) => {
|
||||
};
|
||||
|
||||
export default function EventsPageSection() {
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const backgroundImage = useBackgroundImage("events", "/got-2.jpg");
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/events")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setEvents(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching events:", err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
|
||||
<div className="text-pixel-gold text-xl">Chargement...</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('/got-2.jpg')`,
|
||||
backgroundImage: `url('${backgroundImage}')`,
|
||||
}}
|
||||
>
|
||||
{/* Dark overlay for readability */}
|
||||
@@ -198,17 +168,17 @@ export default function EventsPageSection() {
|
||||
</p>
|
||||
|
||||
{/* Action Button */}
|
||||
{event.status === "upcoming" && (
|
||||
{event.status === "UPCOMING" && (
|
||||
<button className="w-full px-4 py-2 border border-pixel-gold/50 bg-black/40 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition">
|
||||
S'inscrire maintenant
|
||||
</button>
|
||||
)}
|
||||
{event.status === "live" && (
|
||||
{event.status === "LIVE" && (
|
||||
<button className="w-full px-4 py-2 border border-red-500/50 bg-red-900/20 text-red-400 uppercase text-xs tracking-widest rounded hover:bg-red-900/30 transition animate-pulse">
|
||||
Rejoindre en direct
|
||||
</button>
|
||||
)}
|
||||
{event.status === "past" && (
|
||||
{event.status === "PAST" && (
|
||||
<button className="w-full px-4 py-2 border border-gray-600/50 bg-gray-900/20 text-gray-500 uppercase text-xs tracking-widest rounded cursor-not-allowed">
|
||||
Événement terminé
|
||||
</button>
|
||||
|
||||
@@ -1,26 +1,44 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Event {
|
||||
id: string;
|
||||
date: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const events: Event[] = [
|
||||
{
|
||||
date: "18 NOVEMBRE 2023",
|
||||
name: "Sommet de l'Innovation Tech",
|
||||
},
|
||||
{
|
||||
date: "3 DÉCEMBRE 2023",
|
||||
name: "Lancement de la Révolution IA",
|
||||
},
|
||||
{
|
||||
date: "22 DÉCEMBRE 2023",
|
||||
name: "Festival du Code d'Hiver",
|
||||
},
|
||||
];
|
||||
|
||||
export default function EventsSection() {
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/events")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
// Prendre seulement les 3 premiers événements pour la section d'accueil
|
||||
setEvents(data.slice(0, 3));
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching events:", err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="w-full bg-gray-950 border-t border-pixel-gold/30 py-16">
|
||||
<div className="max-w-7xl mx-auto px-8 text-center text-pixel-gold">
|
||||
Chargement...
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
if (events.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<section className="w-full bg-gray-950 border-t border-pixel-gold/30 py-16">
|
||||
<div className="max-w-7xl mx-auto px-8">
|
||||
|
||||
@@ -1,23 +1,196 @@
|
||||
"use client";
|
||||
|
||||
import { useBackgroundImage } from "@/hooks/usePreferences";
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface Particle {
|
||||
width: number;
|
||||
height: number;
|
||||
left: number;
|
||||
top: number;
|
||||
duration: number;
|
||||
delay: number;
|
||||
shadow: number;
|
||||
fadeIn: number;
|
||||
fadeOut: number;
|
||||
visibleDuration: number;
|
||||
moveY1: number;
|
||||
moveX1: number;
|
||||
moveY2: number;
|
||||
moveX2: number;
|
||||
moveY3: number;
|
||||
moveX3: number;
|
||||
moveY4: number;
|
||||
moveX4: number;
|
||||
moveY5: number;
|
||||
moveX5: number;
|
||||
moveY6: number;
|
||||
moveX6: number;
|
||||
}
|
||||
|
||||
interface Orb {
|
||||
width: number;
|
||||
height: number;
|
||||
left: number;
|
||||
top: number;
|
||||
duration: number;
|
||||
delay: number;
|
||||
}
|
||||
|
||||
export default function HeroSection() {
|
||||
const backgroundImage = useBackgroundImage("home", "/got-2.jpg");
|
||||
const [particles, setParticles] = useState<Particle[]>([]);
|
||||
const [orbs, setOrbs] = useState<Orb[]>([]);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
// Generate particles - more visible and dynamic
|
||||
setParticles(
|
||||
Array.from({ length: 30 }, () => {
|
||||
const fadeIn = Math.random() * 5 + 2; // 2-7% of animation - faster fade in
|
||||
const visibleDuration = Math.random() * 30 + 20; // 20-50% of animation
|
||||
const fadeOut = Math.random() * 5 + 2; // 2-7% of animation - faster fade out
|
||||
return {
|
||||
width: Math.random() * 6 + 3,
|
||||
height: Math.random() * 6 + 3,
|
||||
left: Math.random() * 100,
|
||||
top: Math.random() * 100,
|
||||
duration: 10 + Math.random() * 15,
|
||||
delay: Math.random() * 8,
|
||||
shadow: Math.random() * 15 + 8,
|
||||
fadeIn: fadeIn,
|
||||
fadeOut: fadeOut,
|
||||
visibleDuration: visibleDuration,
|
||||
moveY1: 20 + Math.random() * 20,
|
||||
moveX1: Math.random() * 10 - 5,
|
||||
moveY2: 40 + Math.random() * 20,
|
||||
moveX2: Math.random() * 15 - 7,
|
||||
moveY3: 60 + Math.random() * 20,
|
||||
moveX3: Math.random() * 10 - 5,
|
||||
moveY4: 80 + Math.random() * 20,
|
||||
moveX4: Math.random() * 10 - 5,
|
||||
moveY5: 100 + Math.random() * 20,
|
||||
moveX5: Math.random() * 10 - 5,
|
||||
moveY6: 120 + Math.random() * 20,
|
||||
moveX6: Math.random() * 10 - 5,
|
||||
};
|
||||
})
|
||||
);
|
||||
// Generate orbs
|
||||
setOrbs(
|
||||
Array.from({ length: 4 }, () => ({
|
||||
width: 100 + Math.random() * 200,
|
||||
height: 100 + Math.random() * 200,
|
||||
left: Math.random() * 80,
|
||||
top: Math.random() * 80,
|
||||
duration: 20 + Math.random() * 15,
|
||||
delay: Math.random() * 10,
|
||||
}))
|
||||
);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('/got-2.jpg')`,
|
||||
backgroundImage: `url('${backgroundImage}')`,
|
||||
}}
|
||||
>
|
||||
{/* Dark overlay for readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80 z-[1]"></div>
|
||||
|
||||
{/* Animated particles */}
|
||||
{mounted && (
|
||||
<div className="absolute inset-0 overflow-hidden z-[2]">
|
||||
{particles.map((particle, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute rounded-full"
|
||||
style={{
|
||||
width: `${particle.width}px`,
|
||||
height: `${particle.height}px`,
|
||||
left: `${particle.left}%`,
|
||||
top: `${particle.top}%`,
|
||||
background: `radial-gradient(circle, rgba(218, 165, 32, 0.9) 0%, rgba(218, 165, 32, 0.4) 50%, transparent 100%)`,
|
||||
animation: `float-particle-${i} ${particle.duration}s infinite ease-in-out`,
|
||||
animationDelay: `${particle.delay}s`,
|
||||
boxShadow: `0 0 ${
|
||||
particle.shadow
|
||||
}px rgba(218, 165, 32, 0.8), 0 0 ${
|
||||
particle.shadow * 2
|
||||
}px rgba(218, 165, 32, 0.4)`,
|
||||
filter: "blur(0.5px)",
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Animated light rays */}
|
||||
<div className="absolute inset-0 overflow-hidden opacity-30">
|
||||
{[...Array(3)].map((_, i) => {
|
||||
const rotation = -15 + i * 15;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute w-1 bg-gradient-to-b from-transparent via-pixel-gold/20 to-transparent"
|
||||
style={{
|
||||
height: "100%",
|
||||
left: `${20 + i * 30}%`,
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
animation: `light-ray-${i} ${
|
||||
8 + i * 2
|
||||
}s infinite ease-in-out`,
|
||||
animationDelay: `${i * 2}s`,
|
||||
transformOrigin: "top center",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Glowing orbs */}
|
||||
{mounted && (
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
{orbs.map((orb, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute rounded-full blur-xl"
|
||||
style={{
|
||||
width: `${orb.width}px`,
|
||||
height: `${orb.height}px`,
|
||||
left: `${orb.left}%`,
|
||||
top: `${orb.top}%`,
|
||||
background: `radial-gradient(circle, rgba(218, 165, 32, 0.2) 0%, transparent 70%)`,
|
||||
animation: `orb-float ${orb.duration}s infinite ease-in-out`,
|
||||
animationDelay: `${orb.delay}s`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Shimmer effect */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-transparent via-pixel-gold/10 to-transparent"
|
||||
style={{
|
||||
transform: "skewX(-20deg)",
|
||||
animation: "shimmer 8s infinite",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero Content */}
|
||||
<div className="relative z-10 w-full max-w-5xl mx-auto px-8 py-16 text-center">
|
||||
<div className="relative z-10 w-full max-w-5xl xl:max-w-6xl mx-auto px-8 py-16 text-center flex flex-col items-center">
|
||||
{/* Game Title */}
|
||||
<h1 className="text-6xl md:text-8xl lg:text-9xl font-gaming font-black mb-4 tracking-tight">
|
||||
<div className="w-full flex justify-center mb-4">
|
||||
<h1 className="text-6xl md:text-8xl lg:text-9xl xl:text-9xl font-gaming font-black tracking-tight">
|
||||
<span
|
||||
className="bg-gradient-to-r from-pixel-gold via-orange-400 to-pixel-gold bg-clip-text text-transparent"
|
||||
style={{
|
||||
@@ -27,6 +200,7 @@ export default function HeroSection() {
|
||||
GAME.OF.TECH
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Subtitle */}
|
||||
<div className="text-pixel-gold text-xl md:text-2xl font-gaming-subtitle font-semibold flex items-center justify-center gap-2 mb-8 tracking-wider">
|
||||
@@ -46,13 +220,17 @@ export default function HeroSection() {
|
||||
|
||||
{/* Call-to-Action Buttons */}
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 mb-16">
|
||||
<Link href="/events">
|
||||
<button className="px-8 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition">
|
||||
PLAY NOW
|
||||
<span>See events</span>
|
||||
</button>
|
||||
</Link>
|
||||
<Link href="/leaderboard">
|
||||
<button className="px-8 py-3 border border-pixel-gold/50 bg-black/60 text-white uppercase text-sm tracking-widest rounded hover:bg-pixel-gold/10 hover:border-pixel-gold transition flex items-center gap-2">
|
||||
<span>⏵</span>
|
||||
<span>Watch Trailer</span>
|
||||
<span>See leaderboard</span>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -66,6 +244,108 @@ export default function HeroSection() {
|
||||
transform: translateY(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
${particles
|
||||
.map((particle, i) => {
|
||||
const fadeInPercent = particle.fadeIn;
|
||||
const visibleStart = fadeInPercent;
|
||||
const visibleEnd = visibleStart + particle.visibleDuration;
|
||||
const fadeOutStart = visibleEnd;
|
||||
const fadeOutEnd = Math.min(100, fadeOutStart + particle.fadeOut);
|
||||
|
||||
return `
|
||||
@keyframes float-particle-${i} {
|
||||
0% {
|
||||
transform: translateY(0px) translateX(0px) scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
${fadeInPercent}% {
|
||||
transform: translateY(-${particle.moveY1}px) translateX(${particle.moveX1}px) scale(1);
|
||||
opacity: 0.9;
|
||||
}
|
||||
${visibleStart}% {
|
||||
transform: translateY(-${particle.moveY2}px) translateX(${particle.moveX2}px) scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
${visibleEnd}% {
|
||||
transform: translateY(-${particle.moveY3}px) translateX(${particle.moveX3}px) scale(1.05);
|
||||
opacity: 1;
|
||||
}
|
||||
${fadeOutStart}% {
|
||||
transform: translateY(-${particle.moveY4}px) translateX(${particle.moveX4}px) scale(0.9);
|
||||
opacity: 0.7;
|
||||
}
|
||||
${fadeOutEnd}% {
|
||||
transform: translateY(-${particle.moveY5}px) translateX(${particle.moveX5}px) scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-${particle.moveY6}px) translateX(${particle.moveX6}px) scale(0.3);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
|
||||
@keyframes light-ray-0 {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
transform: rotate(-15deg) scaleY(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: rotate(-15deg) scaleY(1.2);
|
||||
}
|
||||
}
|
||||
@keyframes light-ray-1 {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
transform: rotate(0deg) scaleY(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: rotate(0deg) scaleY(1.2);
|
||||
}
|
||||
}
|
||||
@keyframes light-ray-2 {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
transform: rotate(15deg) scaleY(0.8);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
transform: rotate(15deg) scaleY(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes orb-float {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
opacity: 0.2;
|
||||
}
|
||||
33% {
|
||||
transform: translate(30px, -30px) scale(1.1);
|
||||
opacity: 0.3;
|
||||
}
|
||||
66% {
|
||||
transform: translate(-20px, 20px) scale(0.9);
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%) skewX(-20deg);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(200%) skewX(-20deg);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
|
||||
207
components/ImageSelector.tsx
Normal file
207
components/ImageSelector.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
"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>
|
||||
|
||||
<div className="flex gap-4">
|
||||
{/* Colonne gauche - Image */}
|
||||
<div className="flex-shrink-0">
|
||||
{value ? (
|
||||
<div className="relative w-48 h-32 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>
|
||||
) : (
|
||||
<div className="w-48 h-32 border border-pixel-gold/30 rounded bg-black/60 flex items-center justify-center">
|
||||
<span className="text-xs text-gray-500">Aucune</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Colonne droite - Contrôles */}
|
||||
<div className="flex-1 space-y-3">
|
||||
{/* 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>
|
||||
|
||||
{/* Chemin de l'image */}
|
||||
{value && (
|
||||
<p className="text-xs text-gray-400 truncate">{value}</p>
|
||||
)}
|
||||
</div>
|
||||
</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,10 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface LeaderboardEntry {
|
||||
rank: number;
|
||||
username: string;
|
||||
score: number;
|
||||
level: number;
|
||||
avatar?: string | null;
|
||||
}
|
||||
|
||||
// Format number with consistent locale to avoid hydration mismatch
|
||||
@@ -12,20 +15,32 @@ const formatScore = (score: number): string => {
|
||||
return score.toLocaleString("en-US");
|
||||
};
|
||||
|
||||
const mockLeaderboard: LeaderboardEntry[] = [
|
||||
{ rank: 1, username: "DragonSlayer99", score: 125000, level: 85 },
|
||||
{ rank: 2, username: "MineMaster", score: 118500, level: 82 },
|
||||
{ rank: 3, username: "CraftKing", score: 112000, level: 80 },
|
||||
{ rank: 4, username: "PixelWarrior", score: 105500, level: 78 },
|
||||
{ rank: 5, username: "FarminePro", score: 99000, level: 75 },
|
||||
{ rank: 6, username: "GoldDigger", score: 92500, level: 73 },
|
||||
{ rank: 7, username: "EpicGamer", score: 87000, level: 71 },
|
||||
{ rank: 8, username: "Legendary", score: 81500, level: 69 },
|
||||
{ rank: 9, username: "MysticMiner", score: 76000, level: 67 },
|
||||
{ rank: 10, username: "TopPlayer", score: 70500, level: 65 },
|
||||
];
|
||||
|
||||
export default function Leaderboard() {
|
||||
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/leaderboard")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setLeaderboard(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching leaderboard:", err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="w-full bg-black py-16 px-6 border-t-2 border-pixel-dark-purple">
|
||||
<div className="max-w-4xl mx-auto text-center text-pixel-gold">
|
||||
Chargement...
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<section className="w-full bg-black py-16 px-6 border-t-2 border-pixel-dark-purple">
|
||||
<div className="max-w-4xl mx-auto">
|
||||
@@ -44,7 +59,7 @@ export default function Leaderboard() {
|
||||
|
||||
{/* Entries */}
|
||||
<div className="divide-y divide-pixel-gold/10">
|
||||
{mockLeaderboard.map((entry) => (
|
||||
{leaderboard.map((entry) => (
|
||||
<div
|
||||
key={entry.rank}
|
||||
className={`grid grid-cols-12 gap-4 p-4 hover:bg-gray-900/50 transition ${
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useBackgroundImage } from "@/hooks/usePreferences";
|
||||
|
||||
interface LeaderboardEntry {
|
||||
rank: number;
|
||||
username: string;
|
||||
score: number;
|
||||
level: number;
|
||||
avatar?: string;
|
||||
avatar?: string | null;
|
||||
}
|
||||
|
||||
// Format number with consistent locale to avoid hydration mismatch
|
||||
@@ -13,32 +16,41 @@ const formatScore = (score: number): string => {
|
||||
return score.toLocaleString("en-US");
|
||||
};
|
||||
|
||||
const mockLeaderboard: LeaderboardEntry[] = [
|
||||
{ rank: 1, username: "TechMaster2024", score: 125000, level: 85 },
|
||||
{ rank: 2, username: "CodeWarrior", score: 118500, level: 82 },
|
||||
{ rank: 3, username: "AIGenius", score: 112000, level: 80 },
|
||||
{ rank: 4, username: "DevLegend", score: 105500, level: 78 },
|
||||
{ rank: 5, username: "InnovationPro", score: 99000, level: 75 },
|
||||
{ rank: 6, username: "TechNinja", score: 92500, level: 73 },
|
||||
{ rank: 7, username: "DigitalHero", score: 87000, level: 71 },
|
||||
{ rank: 8, username: "CodeCrusher", score: 81500, level: 69 },
|
||||
{ rank: 9, username: "TechWizard", score: 76000, level: 67 },
|
||||
{ rank: 10, username: "InnovationKing", score: 70500, level: 65 },
|
||||
{ rank: 11, username: "DevMaster", score: 68000, level: 64 },
|
||||
{ rank: 12, username: "TechElite", score: 65500, level: 63 },
|
||||
{ rank: 13, username: "CodeChampion", score: 63000, level: 62 },
|
||||
{ rank: 14, username: "AIVisionary", score: 60500, level: 61 },
|
||||
{ rank: 15, username: "TechPioneer", score: 58000, level: 60 },
|
||||
];
|
||||
|
||||
export default function LeaderboardSection() {
|
||||
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const backgroundImage = useBackgroundImage(
|
||||
"leaderboard",
|
||||
"/leaderboard-bg.jpg"
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetch("/api/leaderboard")
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
setLeaderboard(data);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Error fetching leaderboard:", err);
|
||||
setLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
|
||||
<div className="text-pixel-gold text-xl">Chargement...</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center justify-center overflow-hidden pt-24 pb-16">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('/leaderboard-bg.jpg')`,
|
||||
backgroundImage: `url('${backgroundImage}')`,
|
||||
}}
|
||||
>
|
||||
{/* Dark overlay for readability */}
|
||||
@@ -78,7 +90,7 @@ export default function LeaderboardSection() {
|
||||
|
||||
{/* Entries */}
|
||||
<div className="divide-y divide-pixel-gold/10">
|
||||
{mockLeaderboard.map((entry) => (
|
||||
{leaderboard.map((entry) => (
|
||||
<div
|
||||
key={entry.rank}
|
||||
className={`grid grid-cols-12 gap-4 p-4 hover:bg-gray-900/50 transition ${
|
||||
@@ -106,11 +118,21 @@ export default function LeaderboardSection() {
|
||||
|
||||
{/* Player */}
|
||||
<div className="col-span-6 flex items-center gap-3">
|
||||
{entry.avatar ? (
|
||||
<div className="w-10 h-10 rounded-full border border-pixel-gold/30 overflow-hidden">
|
||||
<img
|
||||
src={entry.avatar}
|
||||
alt={entry.username}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-gradient-to-br from-gray-800 to-gray-900 border border-pixel-gold/30 flex items-center justify-center">
|
||||
<span className="text-pixel-gold text-xs font-bold">
|
||||
{entry.username.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<span
|
||||
className={`font-bold ${
|
||||
entry.rank <= 3 ? "text-pixel-gold" : "text-white"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
import PlayerStats from "./PlayerStats";
|
||||
|
||||
export default function Navigation() {
|
||||
const { data: session } = useSession();
|
||||
|
||||
return (
|
||||
<nav className="w-full fixed top-0 left-0 z-50 px-8 py-3 bg-black/80 backdrop-blur-sm border-b border-gray-800/30">
|
||||
<div className="max-w-7xl mx-auto flex items-center justify-between">
|
||||
@@ -14,7 +17,7 @@ export default function Navigation() {
|
||||
</div>
|
||||
<div className="text-pixel-gold text-xs font-gaming-subtitle font-semibold flex items-center gap-1 tracking-wide">
|
||||
<span>✦</span>
|
||||
<span>Game of Tech</span>
|
||||
<span>Peaksys</span>
|
||||
<span>✦</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -39,11 +42,50 @@ export default function Navigation() {
|
||||
>
|
||||
LEADERBOARD
|
||||
</Link>
|
||||
{session?.user?.role === "ADMIN" && (
|
||||
<Link
|
||||
href="/admin"
|
||||
className="text-pixel-gold hover:text-orange-400 transition text-xs font-normal uppercase tracking-widest"
|
||||
>
|
||||
ADMIN
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Player Stats - Right */}
|
||||
<div>
|
||||
{/* Right Side */}
|
||||
<div className="flex items-center gap-4">
|
||||
{session ? (
|
||||
<>
|
||||
<PlayerStats />
|
||||
<Link
|
||||
href="/profile"
|
||||
className="text-white hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest"
|
||||
>
|
||||
PROFIL
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => signOut()}
|
||||
className="text-gray-400 hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest"
|
||||
>
|
||||
Déconnexion
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-white hover:text-pixel-gold transition text-xs font-normal uppercase tracking-widest"
|
||||
>
|
||||
Connexion
|
||||
</Link>
|
||||
<Link
|
||||
href="/register"
|
||||
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 hover:border-pixel-gold transition"
|
||||
>
|
||||
Inscription
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,31 +1,68 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface PlayerStatsProps {
|
||||
username?: string;
|
||||
avatar?: string;
|
||||
hp?: number;
|
||||
maxHp?: number;
|
||||
xp?: number;
|
||||
maxXp?: number;
|
||||
level?: number;
|
||||
}
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
// Format number with consistent locale to avoid hydration mismatch
|
||||
const formatNumber = (num: number): string => {
|
||||
return num.toLocaleString("en-US");
|
||||
};
|
||||
|
||||
export default function PlayerStats({
|
||||
username = "DragonSlayer99",
|
||||
avatar = "/got-2.jpg",
|
||||
hp = 750,
|
||||
maxHp = 1000,
|
||||
xp = 3250,
|
||||
maxXp = 5000,
|
||||
level = 42,
|
||||
}: PlayerStatsProps) {
|
||||
export default function PlayerStats() {
|
||||
const { data: session } = useSession();
|
||||
const [userData, setUserData] = useState({
|
||||
username: "Guest",
|
||||
avatar: null as string | null,
|
||||
hp: 1000,
|
||||
maxHp: 1000,
|
||||
xp: 0,
|
||||
maxXp: 5000,
|
||||
level: 1,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (session?.user?.id) {
|
||||
fetch(`/api/users/${session.user.id}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
setUserData({
|
||||
username: data.username || "Guest",
|
||||
avatar: data.avatar,
|
||||
hp: data.hp || 1000,
|
||||
maxHp: data.maxHp || 1000,
|
||||
xp: data.xp || 0,
|
||||
maxXp: data.maxXp || 5000,
|
||||
level: data.level || 1,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Utiliser les données de session si l'API échoue
|
||||
setUserData({
|
||||
username: session.user.username || "Guest",
|
||||
avatar: null,
|
||||
hp: 1000,
|
||||
maxHp: 1000,
|
||||
xp: 0,
|
||||
maxXp: 5000,
|
||||
level: 1,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
setUserData({
|
||||
username: "Guest",
|
||||
avatar: null,
|
||||
hp: 1000,
|
||||
maxHp: 1000,
|
||||
xp: 0,
|
||||
maxXp: 5000,
|
||||
level: 1,
|
||||
});
|
||||
}
|
||||
}, [session]);
|
||||
|
||||
const { username, avatar, hp, maxHp, xp, maxXp, level } = userData;
|
||||
const [hpPercentage, setHpPercentage] = useState(0);
|
||||
const [xpPercentage, setXpPercentage] = useState(0);
|
||||
|
||||
@@ -56,12 +93,18 @@ export default function PlayerStats({
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Avatar */}
|
||||
<div className="w-10 h-10 rounded-full border border-pixel-gold/20 overflow-hidden bg-gray-900">
|
||||
<div className="w-10 h-10 rounded-full border border-pixel-gold/20 overflow-hidden bg-gray-900 flex items-center justify-center">
|
||||
{avatar ? (
|
||||
<img
|
||||
src={avatar}
|
||||
alt={username}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-pixel-gold text-xs font-bold">
|
||||
{username.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
|
||||
16
components/SessionProvider.tsx
Normal file
16
components/SessionProvider.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
"use client";
|
||||
|
||||
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
|
||||
|
||||
export default function SessionProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<NextAuthSessionProvider basePath="/api/auth">
|
||||
{children}
|
||||
</NextAuthSessionProvider>
|
||||
);
|
||||
}
|
||||
|
||||
555
components/UserManagement.tsx
Normal file
555
components/UserManagement.tsx
Normal file
@@ -0,0 +1,555 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
role: string;
|
||||
score: number;
|
||||
level: number;
|
||||
hp: number;
|
||||
maxHp: number;
|
||||
xp: number;
|
||||
maxXp: number;
|
||||
avatar: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface EditingUser {
|
||||
userId: string;
|
||||
hpDelta: number;
|
||||
xpDelta: number;
|
||||
score: number | null;
|
||||
level: number | null;
|
||||
role: string | null;
|
||||
}
|
||||
|
||||
export default function UserManagement() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [editingUser, setEditingUser] = useState<EditingUser | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/admin/users");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUsers(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching users:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (user: User) => {
|
||||
setEditingUser({
|
||||
userId: user.id,
|
||||
hpDelta: 0,
|
||||
xpDelta: 0,
|
||||
score: user.score,
|
||||
level: user.level,
|
||||
role: user.role,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editingUser) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const body: {
|
||||
hpDelta?: number;
|
||||
xpDelta?: number;
|
||||
score?: number;
|
||||
level?: number;
|
||||
role?: string;
|
||||
} = {};
|
||||
|
||||
if (editingUser.hpDelta !== 0) {
|
||||
body.hpDelta = editingUser.hpDelta;
|
||||
}
|
||||
if (editingUser.xpDelta !== 0) {
|
||||
body.xpDelta = editingUser.xpDelta;
|
||||
}
|
||||
if (editingUser.score !== null) {
|
||||
body.score = editingUser.score;
|
||||
}
|
||||
if (editingUser.level !== null) {
|
||||
body.level = editingUser.level;
|
||||
}
|
||||
if (editingUser.role !== null) {
|
||||
body.role = editingUser.role;
|
||||
}
|
||||
|
||||
const response = await fetch(`/api/admin/users/${editingUser.userId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await fetchUsers();
|
||||
setEditingUser(null);
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || "Erreur lors de la mise à jour");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating user:", error);
|
||||
alert("Erreur lors de la mise à jour");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditingUser(null);
|
||||
};
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
return num.toLocaleString("en-US");
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center text-gray-400 py-8">Chargement...</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{users.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-8">
|
||||
Aucun utilisateur trouvé
|
||||
</div>
|
||||
) : (
|
||||
users.map((user) => {
|
||||
const isEditing = editingUser?.userId === user.id;
|
||||
const previewHp = isEditing
|
||||
? Math.max(0, Math.min(user.maxHp, user.hp + editingUser.hpDelta))
|
||||
: user.hp;
|
||||
const previewXp = isEditing
|
||||
? Math.max(0, user.xp + editingUser.xpDelta)
|
||||
: user.xp;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={user.id}
|
||||
className="bg-black/60 border border-pixel-gold/20 rounded p-3"
|
||||
>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex gap-3 items-center flex-1 min-w-0">
|
||||
{/* Avatar */}
|
||||
<div className="w-10 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-sm font-bold ${user.avatar ? "hidden" : ""}`}>
|
||||
{user.username.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="text-pixel-gold font-bold text-base">
|
||||
{user.username}
|
||||
</h3>
|
||||
<span className="text-xs text-gray-500">Niveau {user.level}</span>
|
||||
<span className="text-xs text-gray-500">Score: {formatNumber(user.score)}</span>
|
||||
<span className={`text-xs ${user.role === "ADMIN" ? "text-pixel-gold" : "text-gray-500"}`}>
|
||||
{user.role}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-400 text-xs truncate">{user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
{!isEditing && (
|
||||
<button
|
||||
onClick={() => handleEdit(user)}
|
||||
className="px-3 py-1.5 border border-pixel-gold/50 bg-black/60 text-white uppercase text-xs tracking-widest rounded hover:bg-pixel-gold/10 transition flex-shrink-0 ml-2"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<div className="space-y-4">
|
||||
{/* HP Section */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="text-sm text-gray-300">
|
||||
Points de Vie (HP)
|
||||
</label>
|
||||
<span className="text-xs text-gray-400">
|
||||
{previewHp} / {user.maxHp}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
hpDelta: editingUser.hpDelta - 100,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||
>
|
||||
-100
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
hpDelta: editingUser.hpDelta - 10,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||
>
|
||||
-10
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
value={editingUser.hpDelta || 0}
|
||||
onChange={(e) =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
hpDelta: parseInt(e.target.value) || 0,
|
||||
})
|
||||
}
|
||||
className="flex-1 px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm text-center"
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
hpDelta: editingUser.hpDelta + 10,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||
>
|
||||
+10
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
hpDelta: editingUser.hpDelta + 100,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||
>
|
||||
+100
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 h-2 bg-black/60 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-red-600 to-green-500 transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(100, (previewHp / user.maxHp) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* XP Section */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="text-sm text-gray-300">
|
||||
Expérience (XP)
|
||||
</label>
|
||||
<span className="text-xs text-gray-400">
|
||||
{formatNumber(previewXp)} / {formatNumber(user.maxXp)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
xpDelta: editingUser.xpDelta - 1000,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||
>
|
||||
-1000
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
xpDelta: editingUser.xpDelta - 100,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||
>
|
||||
-100
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
value={editingUser.xpDelta || 0}
|
||||
onChange={(e) =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
xpDelta: parseInt(e.target.value) || 0,
|
||||
})
|
||||
}
|
||||
className="flex-1 px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm text-center"
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
xpDelta: editingUser.xpDelta + 100,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||
>
|
||||
+100
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
xpDelta: editingUser.xpDelta + 1000,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||
>
|
||||
+1000
|
||||
</button>
|
||||
</div>
|
||||
<div className="mt-2 h-2 bg-black/60 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-blue-600 to-purple-500 transition-all duration-300"
|
||||
style={{
|
||||
width: `${Math.min(100, (previewXp / user.maxXp) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Score Section */}
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-2">
|
||||
Score
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
score: (editingUser.score || 0) - 1000,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||
>
|
||||
-1000
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
score: (editingUser.score || 0) - 100,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||
>
|
||||
-100
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
value={editingUser.score ?? 0}
|
||||
onChange={(e) =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
score: parseInt(e.target.value) || 0,
|
||||
})
|
||||
}
|
||||
className="flex-1 px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm text-center"
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
score: (editingUser.score || 0) + 100,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||
>
|
||||
+100
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
score: (editingUser.score || 0) + 1000,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||
>
|
||||
+1000
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Level Section */}
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-2">
|
||||
Niveau
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
level: Math.max(1, (editingUser.level || 1) - 1),
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-red-500/50 bg-red-900/20 text-red-400 text-xs rounded hover:bg-red-900/30 transition"
|
||||
>
|
||||
-1
|
||||
</button>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
value={editingUser.level ?? 1}
|
||||
onChange={(e) =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
level: Math.max(1, parseInt(e.target.value) || 1),
|
||||
})
|
||||
}
|
||||
className="flex-1 px-3 py-1 bg-black/60 border border-pixel-gold/30 rounded text-white text-sm text-center"
|
||||
/>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
level: (editingUser.level || 1) + 1,
|
||||
})
|
||||
}
|
||||
className="px-3 py-1 border border-green-500/50 bg-green-900/20 text-green-400 text-xs rounded hover:bg-green-900/30 transition"
|
||||
>
|
||||
+1
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Role Section */}
|
||||
<div>
|
||||
<label className="block text-sm text-gray-300 mb-2">
|
||||
Rôle
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
role: "USER",
|
||||
})
|
||||
}
|
||||
className={`flex-1 px-4 py-2 border rounded text-xs uppercase tracking-widest transition ${
|
||||
editingUser.role === "USER"
|
||||
? "border-pixel-gold bg-pixel-gold/20 text-pixel-gold"
|
||||
: "border-gray-600/50 bg-gray-900/20 text-gray-400 hover:bg-gray-900/30"
|
||||
}`}
|
||||
>
|
||||
USER
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
setEditingUser({
|
||||
...editingUser,
|
||||
role: "ADMIN",
|
||||
})
|
||||
}
|
||||
className={`flex-1 px-4 py-2 border rounded text-xs uppercase tracking-widest transition ${
|
||||
editingUser.role === "ADMIN"
|
||||
? "border-pixel-gold bg-pixel-gold/20 text-pixel-gold"
|
||||
: "border-gray-600/50 bg-gray-900/20 text-gray-400 hover:bg-gray-900/30"
|
||||
}`}
|
||||
>
|
||||
ADMIN
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 pt-2">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
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 disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Enregistrement..." : "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="flex gap-4 text-xs">
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between items-center mb-0.5">
|
||||
<span className="text-gray-400">HP</span>
|
||||
<span className="text-gray-400">
|
||||
{user.hp}/{user.maxHp}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-black/60 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-red-600 to-green-500"
|
||||
style={{
|
||||
width: `${Math.min(100, (user.hp / user.maxHp) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex justify-between items-center mb-0.5">
|
||||
<span className="text-gray-400">XP</span>
|
||||
<span className="text-gray-400">
|
||||
{formatNumber(user.xp)}/{formatNumber(user.maxXp)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-1.5 bg-black/60 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-blue-600 to-purple-500"
|
||||
style={{
|
||||
width: `${Math.min(100, (user.xp / user.maxXp) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
71
lib/auth.ts
Normal file
71
lib/auth.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import NextAuth from "next-auth";
|
||||
import Credentials from "next-auth/providers/credentials";
|
||||
import { prisma } from "./prisma";
|
||||
import bcrypt from "bcryptjs";
|
||||
import type { Role } from "@/prisma/generated/prisma/client";
|
||||
|
||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
trustHost: true,
|
||||
providers: [
|
||||
Credentials({
|
||||
credentials: {
|
||||
email: { label: "Email", type: "email" },
|
||||
password: { label: "Password", type: "password" },
|
||||
},
|
||||
authorize: async (credentials) => {
|
||||
if (!credentials?.email || !credentials?.password) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: credentials.email as string },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isPasswordValid = await bcrypt.compare(
|
||||
credentials.password as string,
|
||||
user.password
|
||||
);
|
||||
|
||||
if (!isPasswordValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
role: user.role,
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
callbacks: {
|
||||
jwt: async ({ token, user }) => {
|
||||
if (user) {
|
||||
token.id = user.id;
|
||||
token.username = user.username;
|
||||
token.role = user.role;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
session: async ({ session, token }) => {
|
||||
if (session.user && token) {
|
||||
session.user.id = token.id as string;
|
||||
session.user.username = token.username as string;
|
||||
session.user.role = token.role as Role;
|
||||
}
|
||||
return session;
|
||||
},
|
||||
},
|
||||
pages: {
|
||||
signIn: "/login",
|
||||
},
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
});
|
||||
|
||||
22
lib/prisma.ts
Normal file
22
lib/prisma.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { PrismaClient } from "@/prisma/generated/prisma/client";
|
||||
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
|
||||
|
||||
const adapter = new PrismaBetterSqlite3({
|
||||
url: process.env.DATABASE_URL || "file:./dev.db",
|
||||
});
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
adapter,
|
||||
log:
|
||||
process.env.NODE_ENV === "development"
|
||||
? ["query", "error", "warn"]
|
||||
: ["error"],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
||||
15
package.json
15
package.json
@@ -6,20 +6,33 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"db:seed": "tsx prisma/seed.ts"
|
||||
},
|
||||
"prisma": {
|
||||
"seed": "tsx prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/adapter-better-sqlite3": "^7.1.0",
|
||||
"@prisma/client": "^7.1.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"next": "15.5.7",
|
||||
"next-auth": "5.0.0-beta.30",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bcryptjs": "^3.0.0",
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"postcss": "^8.4.40",
|
||||
"prisma": "^7.1.0",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.6.0"
|
||||
}
|
||||
}
|
||||
|
||||
1369
pnpm-lock.yaml
generated
1369
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
14
prisma.config.ts
Normal file
14
prisma.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// This file was generated by Prisma and assumes you have installed the following:
|
||||
// npm install --save-dev prisma dotenv
|
||||
import "dotenv/config";
|
||||
import { defineConfig, env } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
},
|
||||
datasource: {
|
||||
url: env("DATABASE_URL"),
|
||||
},
|
||||
});
|
||||
39
prisma/generated/prisma/browser.ts
Normal file
39
prisma/generated/prisma/browser.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma-related types and utilities in a browser.
|
||||
* Use it to get access to models, enums, and input types.
|
||||
*
|
||||
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
|
||||
* See `client.ts` for the standard, server-side entry point.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as Prisma from './internal/prismaNamespaceBrowser'
|
||||
export { Prisma }
|
||||
export * as $Enums from './enums'
|
||||
export * from './enums';
|
||||
/**
|
||||
* Model User
|
||||
*
|
||||
*/
|
||||
export type User = Prisma.UserModel
|
||||
/**
|
||||
* Model UserPreferences
|
||||
*
|
||||
*/
|
||||
export type UserPreferences = Prisma.UserPreferencesModel
|
||||
/**
|
||||
* Model Event
|
||||
*
|
||||
*/
|
||||
export type Event = Prisma.EventModel
|
||||
/**
|
||||
* Model SitePreferences
|
||||
*
|
||||
*/
|
||||
export type SitePreferences = Prisma.SitePreferencesModel
|
||||
61
prisma/generated/prisma/client.ts
Normal file
61
prisma/generated/prisma/client.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
|
||||
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import * as process from 'node:process'
|
||||
import * as path from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums"
|
||||
import * as $Class from "./internal/class"
|
||||
import * as Prisma from "./internal/prismaNamespace"
|
||||
|
||||
export * as $Enums from './enums'
|
||||
export * from "./enums"
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Users
|
||||
* const users = await prisma.user.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/client).
|
||||
*/
|
||||
export const PrismaClient = $Class.getPrismaClientClass()
|
||||
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
export { Prisma }
|
||||
|
||||
/**
|
||||
* Model User
|
||||
*
|
||||
*/
|
||||
export type User = Prisma.UserModel
|
||||
/**
|
||||
* Model UserPreferences
|
||||
*
|
||||
*/
|
||||
export type UserPreferences = Prisma.UserPreferencesModel
|
||||
/**
|
||||
* Model Event
|
||||
*
|
||||
*/
|
||||
export type Event = Prisma.EventModel
|
||||
/**
|
||||
* Model SitePreferences
|
||||
*
|
||||
*/
|
||||
export type SitePreferences = Prisma.SitePreferencesModel
|
||||
374
prisma/generated/prisma/commonInputTypes.ts
Normal file
374
prisma/generated/prisma/commonInputTypes.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
import type * as runtime from "@prisma/client/runtime/client"
|
||||
import * as $Enums from "./enums"
|
||||
import type * as Prisma from "./internal/prismaNamespace"
|
||||
|
||||
|
||||
export type StringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
|
||||
export type EnumRoleFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.Role | Prisma.EnumRoleFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.Role[]
|
||||
notIn?: $Enums.Role[]
|
||||
not?: Prisma.NestedEnumRoleFilter<$PrismaModel> | $Enums.Role
|
||||
}
|
||||
|
||||
export type IntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type StringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type DateTimeFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[]
|
||||
notIn?: Date[] | string[]
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||
}
|
||||
|
||||
export type SortOrderInput = {
|
||||
sort: Prisma.SortOrder
|
||||
nulls?: Prisma.NullsOrder
|
||||
}
|
||||
|
||||
export type StringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type EnumRoleWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.Role | Prisma.EnumRoleFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.Role[]
|
||||
notIn?: $Enums.Role[]
|
||||
not?: Prisma.NestedEnumRoleWithAggregatesFilter<$PrismaModel> | $Enums.Role
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedEnumRoleFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedEnumRoleFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type IntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type StringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[]
|
||||
notIn?: Date[] | string[]
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type EnumEventTypeFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.EventType | Prisma.EnumEventTypeFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.EventType[]
|
||||
notIn?: $Enums.EventType[]
|
||||
not?: Prisma.NestedEnumEventTypeFilter<$PrismaModel> | $Enums.EventType
|
||||
}
|
||||
|
||||
export type EnumEventStatusFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.EventStatus | Prisma.EnumEventStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.EventStatus[]
|
||||
notIn?: $Enums.EventStatus[]
|
||||
not?: Prisma.NestedEnumEventStatusFilter<$PrismaModel> | $Enums.EventStatus
|
||||
}
|
||||
|
||||
export type EnumEventTypeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.EventType | Prisma.EnumEventTypeFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.EventType[]
|
||||
notIn?: $Enums.EventType[]
|
||||
not?: Prisma.NestedEnumEventTypeWithAggregatesFilter<$PrismaModel> | $Enums.EventType
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedEnumEventTypeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedEnumEventTypeFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type EnumEventStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.EventStatus | Prisma.EnumEventStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.EventStatus[]
|
||||
notIn?: $Enums.EventStatus[]
|
||||
not?: Prisma.NestedEnumEventStatusWithAggregatesFilter<$PrismaModel> | $Enums.EventStatus
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedEnumEventStatusFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedEnumEventStatusFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedStringFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringFilter<$PrismaModel> | string
|
||||
}
|
||||
|
||||
export type NestedEnumRoleFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.Role | Prisma.EnumRoleFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.Role[]
|
||||
notIn?: $Enums.Role[]
|
||||
not?: Prisma.NestedEnumRoleFilter<$PrismaModel> | $Enums.Role
|
||||
}
|
||||
|
||||
export type NestedIntFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedStringNullableFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableFilter<$PrismaModel> | string | null
|
||||
}
|
||||
|
||||
export type NestedDateTimeFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[]
|
||||
notIn?: Date[] | string[]
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
|
||||
}
|
||||
|
||||
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[]
|
||||
notIn?: string[]
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedEnumRoleWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.Role | Prisma.EnumRoleFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.Role[]
|
||||
notIn?: $Enums.Role[]
|
||||
not?: Prisma.NestedEnumRoleWithAggregatesFilter<$PrismaModel> | $Enums.Role
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedEnumRoleFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedEnumRoleFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
|
||||
_sum?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedFloatFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
in?: number[]
|
||||
notIn?: number[]
|
||||
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
|
||||
}
|
||||
|
||||
export type NestedStringNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> | null
|
||||
in?: string[] | null
|
||||
notIn?: string[] | null
|
||||
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedStringNullableWithAggregatesFilter<$PrismaModel> | string | null
|
||||
_count?: Prisma.NestedIntNullableFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedStringNullableFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedIntNullableFilter<$PrismaModel = never> = {
|
||||
equals?: number | Prisma.IntFieldRefInput<$PrismaModel> | null
|
||||
in?: number[] | null
|
||||
notIn?: number[] | null
|
||||
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedIntNullableFilter<$PrismaModel> | number | null
|
||||
}
|
||||
|
||||
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
in?: Date[] | string[]
|
||||
notIn?: Date[] | string[]
|
||||
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
|
||||
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedEnumEventTypeFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.EventType | Prisma.EnumEventTypeFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.EventType[]
|
||||
notIn?: $Enums.EventType[]
|
||||
not?: Prisma.NestedEnumEventTypeFilter<$PrismaModel> | $Enums.EventType
|
||||
}
|
||||
|
||||
export type NestedEnumEventStatusFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.EventStatus | Prisma.EnumEventStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.EventStatus[]
|
||||
notIn?: $Enums.EventStatus[]
|
||||
not?: Prisma.NestedEnumEventStatusFilter<$PrismaModel> | $Enums.EventStatus
|
||||
}
|
||||
|
||||
export type NestedEnumEventTypeWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.EventType | Prisma.EnumEventTypeFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.EventType[]
|
||||
notIn?: $Enums.EventType[]
|
||||
not?: Prisma.NestedEnumEventTypeWithAggregatesFilter<$PrismaModel> | $Enums.EventType
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedEnumEventTypeFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedEnumEventTypeFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
export type NestedEnumEventStatusWithAggregatesFilter<$PrismaModel = never> = {
|
||||
equals?: $Enums.EventStatus | Prisma.EnumEventStatusFieldRefInput<$PrismaModel>
|
||||
in?: $Enums.EventStatus[]
|
||||
notIn?: $Enums.EventStatus[]
|
||||
not?: Prisma.NestedEnumEventStatusWithAggregatesFilter<$PrismaModel> | $Enums.EventStatus
|
||||
_count?: Prisma.NestedIntFilter<$PrismaModel>
|
||||
_min?: Prisma.NestedEnumEventStatusFilter<$PrismaModel>
|
||||
_max?: Prisma.NestedEnumEventStatusFilter<$PrismaModel>
|
||||
}
|
||||
|
||||
|
||||
36
prisma/generated/prisma/enums.ts
Normal file
36
prisma/generated/prisma/enums.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This file exports all enum related types from the schema.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
|
||||
export const Role = {
|
||||
USER: 'USER',
|
||||
ADMIN: 'ADMIN'
|
||||
} as const
|
||||
|
||||
export type Role = (typeof Role)[keyof typeof Role]
|
||||
|
||||
|
||||
export const EventType = {
|
||||
SUMMIT: 'SUMMIT',
|
||||
LAUNCH: 'LAUNCH',
|
||||
FESTIVAL: 'FESTIVAL',
|
||||
COMPETITION: 'COMPETITION'
|
||||
} as const
|
||||
|
||||
export type EventType = (typeof EventType)[keyof typeof EventType]
|
||||
|
||||
|
||||
export const EventStatus = {
|
||||
UPCOMING: 'UPCOMING',
|
||||
LIVE: 'LIVE',
|
||||
PAST: 'PAST'
|
||||
} as const
|
||||
|
||||
export type EventStatus = (typeof EventStatus)[keyof typeof EventStatus]
|
||||
220
prisma/generated/prisma/internal/class.ts
Normal file
220
prisma/generated/prisma/internal/class.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||
*
|
||||
* Please import the `PrismaClient` class from the `client.ts` file instead.
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/client"
|
||||
import type * as Prisma from "./prismaNamespace"
|
||||
|
||||
|
||||
const config: runtime.GetPrismaClientConfig = {
|
||||
"previewFeatures": [],
|
||||
"clientVersion": "7.1.0",
|
||||
"engineVersion": "ab635e6b9d606fa5c8fb8b1a7f909c3c3c1c98ba",
|
||||
"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\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": {
|
||||
"models": {},
|
||||
"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> {
|
||||
const { Buffer } = await import('node:buffer')
|
||||
const wasmArray = Buffer.from(wasmBase64, 'base64')
|
||||
return new WebAssembly.Module(wasmArray)
|
||||
}
|
||||
|
||||
config.compilerWasm = {
|
||||
getRuntime: async () => await import("@prisma/client/runtime/query_compiler_bg.sqlite.mjs"),
|
||||
|
||||
getQueryCompilerWasmModule: async () => {
|
||||
const { wasm } = await import("@prisma/client/runtime/query_compiler_bg.sqlite.wasm-base64.mjs")
|
||||
return await decodeBase64AsWasm(wasm)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export type LogOptions<ClientOptions extends Prisma.PrismaClientOptions> =
|
||||
'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never
|
||||
|
||||
export interface PrismaClientConstructor {
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Users
|
||||
* const users = await prisma.user.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/client).
|
||||
*/
|
||||
|
||||
new <
|
||||
Options extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
|
||||
LogOpts extends LogOptions<Options> = LogOptions<Options>,
|
||||
OmitOpts extends Prisma.PrismaClientOptions['omit'] = Options extends { omit: infer U } ? U : Prisma.PrismaClientOptions['omit'],
|
||||
ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||
>(options: Prisma.Subset<Options, Prisma.PrismaClientOptions> ): PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
||||
}
|
||||
|
||||
/**
|
||||
* ## Prisma Client
|
||||
*
|
||||
* Type-safe database client for TypeScript
|
||||
* @example
|
||||
* ```
|
||||
* const prisma = new PrismaClient()
|
||||
* // Fetch zero or more Users
|
||||
* const users = await prisma.user.findMany()
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/client).
|
||||
*/
|
||||
|
||||
export interface PrismaClient<
|
||||
in LogOpts extends Prisma.LogLevel = never,
|
||||
in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = undefined,
|
||||
in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs
|
||||
> {
|
||||
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
|
||||
|
||||
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;
|
||||
|
||||
/**
|
||||
* Connect with the database
|
||||
*/
|
||||
$connect(): runtime.Types.Utils.JsPromise<void>;
|
||||
|
||||
/**
|
||||
* Disconnect from the database
|
||||
*/
|
||||
$disconnect(): runtime.Types.Utils.JsPromise<void>;
|
||||
|
||||
/**
|
||||
* Executes a prepared raw query and returns the number of affected rows.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$executeRaw`UPDATE User SET cool = ${true} WHERE email = ${'user@email.com'};`
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||
|
||||
/**
|
||||
* Executes a raw query and returns the number of affected rows.
|
||||
* Susceptible to SQL injections, see documentation.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;
|
||||
|
||||
/**
|
||||
* Performs a prepared raw query and returns the `SELECT` data.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$queryRaw`SELECT * FROM User WHERE id = ${1} OR email = ${'user@email.com'};`
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||
|
||||
/**
|
||||
* Performs a raw query and returns the `SELECT` data.
|
||||
* Susceptible to SQL injections, see documentation.
|
||||
* @example
|
||||
* ```
|
||||
* const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://pris.ly/d/raw-queries).
|
||||
*/
|
||||
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;
|
||||
|
||||
|
||||
/**
|
||||
* Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
|
||||
* @example
|
||||
* ```
|
||||
* const [george, bob, alice] = await prisma.$transaction([
|
||||
* prisma.user.create({ data: { name: 'George' } }),
|
||||
* prisma.user.create({ data: { name: 'Bob' } }),
|
||||
* prisma.user.create({ data: { name: 'Alice' } }),
|
||||
* ])
|
||||
* ```
|
||||
*
|
||||
* Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
|
||||
*/
|
||||
$transaction<P extends Prisma.PrismaPromise<any>[]>(arg: [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<runtime.Types.Utils.UnwrapTuple<P>>
|
||||
|
||||
$transaction<R>(fn: (prisma: Omit<PrismaClient, runtime.ITXClientDenyList>) => runtime.Types.Utils.JsPromise<R>, options?: { maxWait?: number, timeout?: number, isolationLevel?: Prisma.TransactionIsolationLevel }): runtime.Types.Utils.JsPromise<R>
|
||||
|
||||
$extends: runtime.Types.Extensions.ExtendsHook<"extends", Prisma.TypeMapCb<OmitOpts>, ExtArgs, runtime.Types.Utils.Call<Prisma.TypeMapCb<OmitOpts>, {
|
||||
extArgs: ExtArgs
|
||||
}>>
|
||||
|
||||
/**
|
||||
* `prisma.user`: Exposes CRUD operations for the **User** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Users
|
||||
* const users = await prisma.user.findMany()
|
||||
* ```
|
||||
*/
|
||||
get user(): Prisma.UserDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.userPreferences`: Exposes CRUD operations for the **UserPreferences** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more UserPreferences
|
||||
* const userPreferences = await prisma.userPreferences.findMany()
|
||||
* ```
|
||||
*/
|
||||
get userPreferences(): Prisma.UserPreferencesDelegate<ExtArgs, { omit: OmitOpts }>;
|
||||
|
||||
/**
|
||||
* `prisma.event`: Exposes CRUD operations for the **Event** model.
|
||||
* Example usage:
|
||||
* ```ts
|
||||
* // Fetch zero or more Events
|
||||
* const events = await prisma.event.findMany()
|
||||
* ```
|
||||
*/
|
||||
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 {
|
||||
return runtime.getPrismaClient(config) as unknown as PrismaClientConstructor
|
||||
}
|
||||
1033
prisma/generated/prisma/internal/prismaNamespace.ts
Normal file
1033
prisma/generated/prisma/internal/prismaNamespace.ts
Normal file
File diff suppressed because it is too large
Load Diff
147
prisma/generated/prisma/internal/prismaNamespaceBrowser.ts
Normal file
147
prisma/generated/prisma/internal/prismaNamespaceBrowser.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* WARNING: This is an internal file that is subject to change!
|
||||
*
|
||||
* 🛑 Under no circumstances should you import this file directly! 🛑
|
||||
*
|
||||
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
|
||||
* While this enables partial backward compatibility, it is not part of the stable public API.
|
||||
*
|
||||
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
|
||||
* model files in the `model` directory!
|
||||
*/
|
||||
|
||||
import * as runtime from "@prisma/client/runtime/index-browser"
|
||||
|
||||
export type * from '../models'
|
||||
export type * from './prismaNamespace'
|
||||
|
||||
export const Decimal = runtime.Decimal
|
||||
|
||||
|
||||
export const NullTypes = {
|
||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
||||
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
||||
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
||||
}
|
||||
/**
|
||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const DbNull = runtime.DbNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const JsonNull = runtime.JsonNull
|
||||
|
||||
/**
|
||||
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||
*
|
||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||
*/
|
||||
export const AnyNull = runtime.AnyNull
|
||||
|
||||
|
||||
export const ModelName = {
|
||||
User: 'User',
|
||||
UserPreferences: 'UserPreferences',
|
||||
Event: 'Event',
|
||||
SitePreferences: 'SitePreferences'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
|
||||
/*
|
||||
* Enums
|
||||
*/
|
||||
|
||||
export const TransactionIsolationLevel = {
|
||||
Serializable: 'Serializable'
|
||||
} as const
|
||||
|
||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
||||
|
||||
|
||||
export const UserScalarFieldEnum = {
|
||||
id: 'id',
|
||||
email: 'email',
|
||||
password: 'password',
|
||||
username: 'username',
|
||||
role: 'role',
|
||||
score: 'score',
|
||||
level: 'level',
|
||||
hp: 'hp',
|
||||
maxHp: 'maxHp',
|
||||
xp: 'xp',
|
||||
maxXp: 'maxXp',
|
||||
avatar: 'avatar',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
||||
|
||||
|
||||
export const UserPreferencesScalarFieldEnum = {
|
||||
id: 'id',
|
||||
userId: 'userId',
|
||||
homeBackground: 'homeBackground',
|
||||
eventsBackground: 'eventsBackground',
|
||||
leaderboardBackground: 'leaderboardBackground',
|
||||
theme: 'theme',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
export type UserPreferencesScalarFieldEnum = (typeof UserPreferencesScalarFieldEnum)[keyof typeof UserPreferencesScalarFieldEnum]
|
||||
|
||||
|
||||
export const EventScalarFieldEnum = {
|
||||
id: 'id',
|
||||
date: 'date',
|
||||
name: 'name',
|
||||
description: 'description',
|
||||
type: 'type',
|
||||
status: 'status',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
|
||||
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 = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
} as const
|
||||
|
||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
||||
|
||||
|
||||
export const NullsOrder = {
|
||||
first: 'first',
|
||||
last: 'last'
|
||||
} as const
|
||||
|
||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
||||
|
||||
15
prisma/generated/prisma/models.ts
Normal file
15
prisma/generated/prisma/models.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||
/* eslint-disable */
|
||||
// biome-ignore-all lint: generated file
|
||||
// @ts-nocheck
|
||||
/*
|
||||
* This is a barrel export file for all models and their related types.
|
||||
*
|
||||
* 🟢 You can import this file directly.
|
||||
*/
|
||||
export type * from './models/User'
|
||||
export type * from './models/UserPreferences'
|
||||
export type * from './models/Event'
|
||||
export type * from './models/SitePreferences'
|
||||
export type * from './commonInputTypes'
|
||||
1234
prisma/generated/prisma/models/Event.ts
Normal file
1234
prisma/generated/prisma/models/Event.ts
Normal file
File diff suppressed because it is too large
Load Diff
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
1670
prisma/generated/prisma/models/User.ts
Normal file
1670
prisma/generated/prisma/models/User.ts
Normal file
File diff suppressed because it is too large
Load Diff
1384
prisma/generated/prisma/models/UserPreferences.ts
Normal file
1384
prisma/generated/prisma/models/UserPreferences.ts
Normal file
File diff suppressed because it is too large
Load Diff
63
prisma/migrations/20251209055617_init/migration.sql
Normal file
63
prisma/migrations/20251209055617_init/migration.sql
Normal file
@@ -0,0 +1,63 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "User" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"email" TEXT NOT NULL,
|
||||
"password" TEXT NOT NULL,
|
||||
"username" TEXT NOT NULL,
|
||||
"role" TEXT NOT NULL DEFAULT 'USER',
|
||||
"score" INTEGER NOT NULL DEFAULT 0,
|
||||
"level" INTEGER NOT NULL DEFAULT 1,
|
||||
"hp" INTEGER NOT NULL DEFAULT 1000,
|
||||
"maxHp" INTEGER NOT NULL DEFAULT 1000,
|
||||
"xp" INTEGER NOT NULL DEFAULT 0,
|
||||
"maxXp" INTEGER NOT NULL DEFAULT 5000,
|
||||
"avatar" TEXT,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "UserPreferences" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"homeBackground" TEXT,
|
||||
"eventsBackground" TEXT,
|
||||
"leaderboardBackground" TEXT,
|
||||
"theme" TEXT DEFAULT 'default',
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "UserPreferences_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "Event" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"date" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"description" TEXT NOT NULL,
|
||||
"type" TEXT NOT NULL,
|
||||
"status" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_score_idx" ON "User"("score");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "User_email_idx" ON "User"("email");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "UserPreferences_userId_key" ON "UserPreferences"("userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Event_status_idx" ON "Event"("status");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Event_date_idx" ON "Event"("date");
|
||||
@@ -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
|
||||
);
|
||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@@ -0,0 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "sqlite"
|
||||
90
prisma/schema.prisma
Normal file
90
prisma/schema.prisma
Normal file
@@ -0,0 +1,90 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "./generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
}
|
||||
|
||||
enum Role {
|
||||
USER
|
||||
ADMIN
|
||||
}
|
||||
|
||||
enum EventType {
|
||||
SUMMIT
|
||||
LAUNCH
|
||||
FESTIVAL
|
||||
COMPETITION
|
||||
}
|
||||
|
||||
enum EventStatus {
|
||||
UPCOMING
|
||||
LIVE
|
||||
PAST
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
email String @unique
|
||||
password String
|
||||
username String @unique
|
||||
role Role @default(USER)
|
||||
score Int @default(0)
|
||||
level Int @default(1)
|
||||
hp Int @default(1000)
|
||||
maxHp Int @default(1000)
|
||||
xp Int @default(0)
|
||||
maxXp Int @default(5000)
|
||||
avatar String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
preferences UserPreferences?
|
||||
|
||||
@@index([score])
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
model UserPreferences {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
// Background images for each page
|
||||
homeBackground String?
|
||||
eventsBackground String?
|
||||
leaderboardBackground String?
|
||||
|
||||
// Other UI preferences can be added here
|
||||
theme String? @default("default")
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Event {
|
||||
id String @id @default(cuid())
|
||||
date String
|
||||
name String
|
||||
description String
|
||||
type EventType
|
||||
status EventStatus
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([status])
|
||||
@@index([date])
|
||||
}
|
||||
|
||||
model SitePreferences {
|
||||
id String @id @default("global")
|
||||
homeBackground String?
|
||||
eventsBackground String?
|
||||
leaderboardBackground String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
144
prisma/seed.ts
Normal file
144
prisma/seed.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import {
|
||||
PrismaClient,
|
||||
EventType,
|
||||
EventStatus,
|
||||
} from "@/prisma/generated/prisma/client";
|
||||
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
const adapter = new PrismaBetterSqlite3({
|
||||
url: process.env.DATABASE_URL || "file:./dev.db",
|
||||
});
|
||||
const prisma = new PrismaClient({ adapter });
|
||||
|
||||
async function main() {
|
||||
// Créer un utilisateur admin
|
||||
const adminPassword = await bcrypt.hash("admin123", 10);
|
||||
const admin = await prisma.user.upsert({
|
||||
where: { email: "admin@got-mc.com" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "admin@got-mc.com",
|
||||
username: "Admin",
|
||||
password: adminPassword,
|
||||
role: "ADMIN",
|
||||
score: 0,
|
||||
level: 1,
|
||||
},
|
||||
});
|
||||
|
||||
// Créer quelques utilisateurs de test
|
||||
const userPassword = await bcrypt.hash("user123", 10);
|
||||
const users = await Promise.all([
|
||||
prisma.user.upsert({
|
||||
where: { email: "user1@got-mc.com" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "user1@got-mc.com",
|
||||
username: "DragonSlayer99",
|
||||
password: userPassword,
|
||||
score: 125000,
|
||||
level: 85,
|
||||
hp: 750,
|
||||
maxHp: 1000,
|
||||
xp: 3250,
|
||||
maxXp: 5000,
|
||||
},
|
||||
}),
|
||||
prisma.user.upsert({
|
||||
where: { email: "user2@got-mc.com" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "user2@got-mc.com",
|
||||
username: "MineMaster",
|
||||
password: userPassword,
|
||||
score: 118500,
|
||||
level: 82,
|
||||
},
|
||||
}),
|
||||
prisma.user.upsert({
|
||||
where: { email: "user3@got-mc.com" },
|
||||
update: {},
|
||||
create: {
|
||||
email: "user3@got-mc.com",
|
||||
username: "CraftKing",
|
||||
password: userPassword,
|
||||
score: 112000,
|
||||
level: 80,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// Créer des événements (vérifier s'ils existent déjà)
|
||||
const eventData = [
|
||||
{
|
||||
date: "18 NOVEMBRE 2023",
|
||||
name: "Sommet de l'Innovation Tech",
|
||||
description:
|
||||
"Rejoignez les leaders de l'industrie et les innovateurs pour une journée de discussions sur les technologies de pointe, les percées de l'IA et des opportunités de networking.",
|
||||
type: EventType.SUMMIT,
|
||||
status: EventStatus.PAST,
|
||||
},
|
||||
{
|
||||
date: "3 DÉCEMBRE 2023",
|
||||
name: "Lancement de la Révolution IA",
|
||||
description:
|
||||
"Assistez au lancement de systèmes d'IA révolutionnaires qui vont remodeler le paysage du gaming. Aperçus exclusifs et opportunités d'accès anticipé.",
|
||||
type: EventType.LAUNCH,
|
||||
status: EventStatus.PAST,
|
||||
},
|
||||
{
|
||||
date: "22 DÉCEMBRE 2023",
|
||||
name: "Festival du Code d'Hiver",
|
||||
description:
|
||||
"Une célébration de l'excellence en programmation avec des hackathons, des défis de codage et des prix. Montrez vos compétences et rivalisez avec les meilleurs développeurs.",
|
||||
type: EventType.FESTIVAL,
|
||||
status: EventStatus.PAST,
|
||||
},
|
||||
{
|
||||
date: "15 JANVIER 2024",
|
||||
name: "Expo Informatique Quantique",
|
||||
description:
|
||||
"Explorez l'avenir de l'informatique quantique dans le gaming. Démonstrations interactives, conférences d'experts et ateliers pratiques pour tous les niveaux.",
|
||||
type: EventType.SUMMIT,
|
||||
status: EventStatus.UPCOMING,
|
||||
},
|
||||
{
|
||||
date: "8 FÉVRIER 2024",
|
||||
name: "Championnat Cyber Arena",
|
||||
description:
|
||||
"L'événement de gaming compétitif ultime. Compétissez pour la gloire, des récompenses exclusives et le titre de Champion Cyber Arena. Inscriptions ouvertes.",
|
||||
type: EventType.COMPETITION,
|
||||
status: EventStatus.UPCOMING,
|
||||
},
|
||||
{
|
||||
date: "12 MARS 2024",
|
||||
name: "Gala Tech du Printemps",
|
||||
description:
|
||||
"Une soirée élégante célébrant les réalisations technologiques. Cérémonie de remise de prix, networking et annonces exclusives des plus grandes entreprises tech.",
|
||||
type: EventType.FESTIVAL,
|
||||
status: EventStatus.UPCOMING,
|
||||
},
|
||||
];
|
||||
|
||||
const events = await Promise.all(
|
||||
eventData.map(async (data) => {
|
||||
const existing = await prisma.event.findFirst({
|
||||
where: { name: data.name },
|
||||
});
|
||||
if (existing) return existing;
|
||||
return prisma.event.create({ data });
|
||||
})
|
||||
);
|
||||
|
||||
console.log("Seed completed:", { admin, users, events });
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
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 |
BIN
public/uploads/1765281255439-got-light.jpg
Normal file
BIN
public/uploads/1765281255439-got-light.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 916 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 MiB |
27
types/next-auth.d.ts
vendored
Normal file
27
types/next-auth.d.ts
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Role } from "@/prisma/generated/prisma/client";
|
||||
import "next-auth";
|
||||
|
||||
declare module "next-auth" {
|
||||
interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
role: Role;
|
||||
}
|
||||
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
role: Role;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
id: string;
|
||||
username: string;
|
||||
role: Role;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user