Compare commits

...

10 Commits

Author SHA1 Message Date
Julien Froidefond
b1f36f6210 Enhance Admin and Profile UI: Update admin page to display user preferences with improved layout and visuals. Add password change functionality to profile page, including form handling and validation. Refactor ImageSelector for better image preview and upload experience. 2025-12-09 14:02:27 +01:00
Julien Froidefond
4e38bd1e8e Refactor Navigation component to improve user experience by adding profile link for user access and enhancing navigation options. 2025-12-09 12:56:24 +01:00
Julien Froidefond
447ef9d076 Add profile link to Navigation component for user access, enhancing navigation options. 2025-12-09 12:46:29 +01:00
Julien Froidefond
f732eb7385 feat: user admin ui : condensed 2025-12-09 12:40:45 +01:00
Julien Froidefond
d1e94f1402 Update HeroSection buttons to use Next.js Link for navigation to events and leaderboard, enhancing user experience and accessibility. Rename game title in Navigation component to 'Peaksys'. 2025-12-09 08:51:24 +01:00
Julien Froidefond
82c557e10c Add Event Management section to admin page, allowing users to manage events alongside preferences and user management. Updated UI to include event button and corresponding display area. 2025-12-09 08:49:47 +01:00
Julien Froidefond
4de3fea776 Add User Management component to admin page, replacing placeholder text with functional UI for user management. 2025-12-09 08:48:08 +01:00
Julien Froidefond
8c326bdd20 Refactor admin preferences management to use global site preferences, update UI components for better user experience, and implement image selection for background settings. 2025-12-09 08:37:52 +01:00
Julien Froidefond
4486f305f2 Add database and Prisma configurations, enhance event and leaderboard components with API integration, and update navigation for session management 2025-12-09 08:24:14 +01:00
Julien Froidefond
f57a30eb4d Enhance HeroSection layout by adding flexbox for better alignment and adjusting max-width for larger screens. This improves the overall presentation of the game title. 2025-12-09 06:53:27 +01:00
64 changed files with 13237 additions and 190 deletions

10
.env Normal file
View 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
View File

@@ -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
View 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>
);
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View File

@@ -0,0 +1,4 @@
import { handlers } from "@/lib/auth";
export const { GET, POST } = handlers;

21
app/api/events/route.ts Normal file
View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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 }
);
}
}

View 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
View 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
View 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 }
);
}
}

View 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 }
);
}
}

View File

@@ -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
View 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
View 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
View 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>
);
}

View 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>
);
}

View File

@@ -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>

View File

@@ -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">

View File

@@ -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>
);

View 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>
);
}

View File

@@ -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 ${

View File

@@ -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"

View File

@@ -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>

View File

@@ -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 */}

View 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>
);
}

View 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
View 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
View 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
View 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;

View File

@@ -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

File diff suppressed because it is too large Load Diff

14
prisma.config.ts Normal file
View 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"),
},
});

View 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

View 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

View 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>
}

View 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]

View 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
}

File diff suppressed because it is too large Load Diff

View 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]

View 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'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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");

View File

@@ -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
);

View 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
View 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
View 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
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

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
View 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;
}
}