feat: add user accounts, authentication, and game history with Prisma 7

- Implement user registration and login with NextAuth v5 (email/password, JWT)
- Add authentication modal in UI with login/register tabs
- Create user profile screen showing game statistics and history
- Integrate Prisma 7 ORM with SQLite database for data persistence
- Store game results (mode, path, clicks, time) in database
- Auto-save completed games only when user is authenticated
- Separate business logic into reusable hooks (useSoloGame, useMultiGame)
- Organize UI into composable screen components (HomeScreen, SoloScreen, ProfileScreen, etc)
- Add session persistence across F5 refresh for solo and multiplayer
- Style auth modal, account button, and profile stats dashboard
This commit is contained in:
jessy-david-dev
2026-04-10 15:43:07 +02:00
parent 6a75e80e6c
commit dc09658fdb
44 changed files with 12371 additions and 91 deletions
+113
View File
@@ -0,0 +1,113 @@
import type { WikiArticle, Puzzle } from "./types";
import { getFallbackPuzzle } from "./puzzles";
const WIKI_API_BASE = "https://fr.wikipedia.org/w/api.php";
const MIN_ARTICLE_BYTES = 10000;
const BAD_TITLE_PREFIXES = ["Liste de", "Liste des", "Index de", "Portail:"];
const BAD_TITLE_SUFFIXES = ["(homonymie)", "(disambiguation)"];
// Cache de promesses module-level
const articleCache = new Map<string, Promise<WikiArticle | null>>();
function doFetchArticle(title: string): Promise<WikiArticle | null> {
const params = new URLSearchParams({
action: "parse",
page: title,
format: "json",
origin: "*",
prop: "text|displaytitle",
disableeditsection: "1",
redirects: "1",
});
return fetch(`${WIKI_API_BASE}?${params}`)
.then((res) => {
if (!res.ok) throw new Error("Erreur reseau");
return res.json();
})
.then((data): WikiArticle => {
if (data.error) throw new Error(data.error.info ?? "Article introuvable");
return {
html: data.parse.text["*"] as string,
title: data.parse.title as string,
};
})
.catch((err) => {
articleCache.delete(title);
throw err;
});
}
function getCachedArticle(title: string): Promise<WikiArticle | null> {
if (!articleCache.has(title)) {
articleCache.set(title, doFetchArticle(title));
}
return articleCache.get(title)!;
}
export function prefetchArticle(title: string): void {
getCachedArticle(title).catch(() => {});
}
export async function fetchArticle(title: string): Promise<WikiArticle | null> {
try {
return await getCachedArticle(title);
} catch {
return null;
}
}
interface WikiPageInfo {
title: string;
length: number;
}
function isGoodArticle(page: WikiPageInfo): boolean {
const t = page.title;
return (
page.length >= MIN_ARTICLE_BYTES &&
!BAD_TITLE_PREFIXES.some((p) => t.startsWith(p)) &&
!BAD_TITLE_SUFFIXES.some((s) => t.endsWith(s))
);
}
async function fetchRandomCandidates(): Promise<string[]> {
const params = new URLSearchParams({
action: "query",
generator: "random",
grnnamespace: "0",
grnlimit: "50",
grnfilterredir: "nonredirects",
prop: "info",
format: "json",
origin: "*",
});
const res = await fetch(`${WIKI_API_BASE}?${params}`);
if (!res.ok) throw new Error("Erreur reseau");
const data = await res.json() as { query: { pages: Record<string, WikiPageInfo> } };
return Object.values(data.query.pages)
.filter(isGoodArticle)
.map((p) => p.title);
}
export async function pickTwoArticles(): Promise<Puzzle> {
try {
const collected: string[] = [];
for (let i = 0; i < 3; i++) {
const batch = await fetchRandomCandidates();
for (const title of batch) {
if (!collected.includes(title)) collected.push(title);
if (collected.length >= 2) return { start: collected[0], target: collected[1] };
}
}
} catch {
// Fallback si l'API est indisponible
}
return getFallbackPuzzle();
}
export function normalizeTitle(s: string): string {
return decodeURIComponent(s).replace(/_/g, " ").toLowerCase().trim();
}
export const POLL_INTERVAL = 2000;
export const COUNTDOWN_DURATION = 3000;