feat: ajout structure projet + config Node 20

This commit is contained in:
UltraLionFr
2025-12-07 00:11:21 +01:00
parent c728019f04
commit 25d49d04f0
60 changed files with 4260 additions and 157 deletions
+60
View File
@@ -0,0 +1,60 @@
import NextAuth from "next-auth";
import Discord from "next-auth/providers/discord";
const ALLOWED_ADMIN_IDS = process.env.ALLOWED_DISCORD_IDS?.split(",") || [];
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Discord({
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
authorization: {
params: {
scope: "identify",
},
},
}),
],
callbacks: {
async signIn({ account }) {
if (account?.provider === "discord") {
const discordId = account.providerAccountId;
if (!ALLOWED_ADMIN_IDS.includes(discordId)) {
return "/login?error=unauthorized";
}
}
return true;
},
async session({ session, token }) {
if (token.sub) {
session.user.id = token.sub;
}
if (token.discordId) {
session.user.discordId = token.discordId as string;
}
return session;
},
async jwt({ token, account }) {
if (account?.provider === "discord") {
token.discordId = account.providerAccountId;
}
return token;
},
},
pages: {
signIn: "/login",
error: "/login",
},
});
declare module "next-auth" {
interface Session {
user: {
id: string;
discordId?: string;
name?: string | null;
email?: string | null;
image?: string | null;
};
}
}
+18
View File
@@ -0,0 +1,18 @@
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma?: PrismaClient;
};
export const prisma =
globalForPrisma.prisma ??
new PrismaClient({
log:
process.env.NODE_ENV === "development"
? ["query", "error", "warn"]
: ["error"],
});
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
+314
View File
@@ -0,0 +1,314 @@
import { prisma } from "./prisma";
export type Difficulty = "Débutant" | "Intermédiaire" | "Avancé";
export type Category = {
id: string;
name: string;
slug: string;
color: string;
};
export type Tutorial = {
id: string;
slug: string;
title: string;
categoryId: string;
category: Category;
difficulty: Difficulty;
readTime: string;
excerpt: string;
content: string;
date: string;
updatedAt: string;
};
export type TutorialInput = {
slug: string;
title: string;
categoryId: string;
difficulty: Difficulty;
readTime?: string;
excerpt?: string;
content: string;
};
/**
* Génère un slug à partir d'un titre
*/
export function generateSlug(title: string): string {
return title
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/[^a-z0-9\s-]/g, "")
.trim()
.replace(/\s+/g, "-")
.replace(/-+/g, "-");
}
// ============ TUTORIALS ============
export async function getAllTutorials(): Promise<Tutorial[]> {
const rows = await prisma.tutorial.findMany({
orderBy: { date: "desc" },
include: { category: true },
});
return rows.map((row) => ({
id: row.id,
slug: row.slug,
title: row.title,
categoryId: row.categoryId,
category: {
id: row.category.id,
name: row.category.name,
slug: row.category.slug,
color: row.category.color,
},
difficulty: row.difficulty as Difficulty,
readTime: row.readTime ?? "",
excerpt: row.excerpt ?? "",
content: row.content,
date: row.date.toISOString(),
updatedAt: row.updatedAt.toISOString(),
}));
}
export async function getTutorialBySlug(
slug: string
): Promise<Tutorial | null> {
const row = await prisma.tutorial.findUnique({
where: { slug },
include: { category: true },
});
if (!row) return null;
return {
id: row.id,
slug: row.slug,
title: row.title,
categoryId: row.categoryId,
category: {
id: row.category.id,
name: row.category.name,
slug: row.category.slug,
color: row.category.color,
},
difficulty: row.difficulty as Difficulty,
readTime: row.readTime ?? "",
excerpt: row.excerpt ?? "",
content: row.content,
date: row.date.toISOString(),
updatedAt: row.updatedAt.toISOString(),
};
}
export async function getTutorialById(id: string): Promise<Tutorial | null> {
const row = await prisma.tutorial.findUnique({
where: { id },
include: { category: true },
});
if (!row) return null;
return {
id: row.id,
slug: row.slug,
title: row.title,
categoryId: row.categoryId,
category: {
id: row.category.id,
name: row.category.name,
slug: row.category.slug,
color: row.category.color,
},
difficulty: row.difficulty as Difficulty,
readTime: row.readTime ?? "",
excerpt: row.excerpt ?? "",
content: row.content,
date: row.date.toISOString(),
updatedAt: row.updatedAt.toISOString(),
};
}
export async function addTutorial(data: TutorialInput): Promise<Tutorial> {
const row = await prisma.tutorial.create({
data: {
slug: data.slug,
title: data.title,
categoryId: data.categoryId,
difficulty: data.difficulty,
readTime: data.readTime || null,
excerpt: data.excerpt || null,
content: data.content,
},
include: { category: true },
});
return {
id: row.id,
slug: row.slug,
title: row.title,
categoryId: row.categoryId,
category: {
id: row.category.id,
name: row.category.name,
slug: row.category.slug,
color: row.category.color,
},
difficulty: row.difficulty as Difficulty,
readTime: row.readTime ?? "",
excerpt: row.excerpt ?? "",
content: row.content,
date: row.date.toISOString(),
updatedAt: row.updatedAt.toISOString(),
};
}
export async function updateTutorial(
id: string,
data: Partial<TutorialInput>
): Promise<Tutorial> {
const row = await prisma.tutorial.update({
where: { id },
data: {
...(data.slug && { slug: data.slug }),
...(data.title && { title: data.title }),
...(data.categoryId && { categoryId: data.categoryId }),
...(data.difficulty && { difficulty: data.difficulty }),
...(data.readTime !== undefined && {
readTime: data.readTime || null,
}),
...(data.excerpt !== undefined && {
excerpt: data.excerpt || null,
}),
...(data.content && { content: data.content }),
},
include: { category: true },
});
return {
id: row.id,
slug: row.slug,
title: row.title,
categoryId: row.categoryId,
category: {
id: row.category.id,
name: row.category.name,
slug: row.category.slug,
color: row.category.color,
},
difficulty: row.difficulty as Difficulty,
readTime: row.readTime ?? "",
excerpt: row.excerpt ?? "",
content: row.content,
date: row.date.toISOString(),
updatedAt: row.updatedAt.toISOString(),
};
}
export async function deleteTutorial(id: string): Promise<void> {
await prisma.tutorial.delete({
where: { id },
});
}
// ============ CATEGORIES ============
export async function getAllCategories(): Promise<Category[]> {
const rows = await prisma.category.findMany({
orderBy: { name: "asc" },
});
return rows.map((row) => ({
id: row.id,
name: row.name,
slug: row.slug,
color: row.color,
}));
}
export async function getCategoryById(id: string): Promise<Category | null> {
const row = await prisma.category.findUnique({
where: { id },
});
if (!row) return null;
return {
id: row.id,
name: row.name,
slug: row.slug,
color: row.color,
};
}
export async function addCategory(
name: string,
color?: string
): Promise<Category> {
const row = await prisma.category.create({
data: {
name,
slug: generateSlug(name),
color: color || "#06b6d4",
},
});
return {
id: row.id,
name: row.name,
slug: row.slug,
color: row.color,
};
}
export async function updateCategory(
id: string,
data: { name?: string; color?: string }
): Promise<Category> {
const row = await prisma.category.update({
where: { id },
data: {
...(data.name && {
name: data.name,
slug: generateSlug(data.name),
}),
...(data.color && { color: data.color }),
},
});
return {
id: row.id,
name: row.name,
slug: row.slug,
color: row.color,
};
}
export async function deleteCategory(id: string): Promise<void> {
// Vérifier qu'aucun tutoriel n'utilise cette catégorie
const count = await prisma.tutorial.count({
where: { categoryId: id },
});
if (count > 0) {
throw new Error(
`Impossible de supprimer: ${count} tutoriel(s) utilisent cette catégorie`
);
}
await prisma.category.delete({
where: { id },
});
}
export async function getCategoryTutorialCount(
categoryId: string
): Promise<number> {
return prisma.tutorial.count({
where: { categoryId },
});
}