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:
+113
@@ -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;
|
||||
Reference in New Issue
Block a user