feat: add daily challenge and blitz mode (2min speedrun)
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { prisma } from "../../../../lib/prisma";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const date = new Date().toISOString().slice(0, 10);
|
||||||
|
const puzzle = await prisma.dailyPuzzle.findUnique({ where: { date } });
|
||||||
|
if (!puzzle) return NextResponse.json([]);
|
||||||
|
|
||||||
|
const results = await prisma.dailyResult.findMany({
|
||||||
|
where: { puzzleId: puzzle.id, won: true },
|
||||||
|
include: { user: { select: { name: true } } },
|
||||||
|
orderBy: [{ clicks: "asc" }, { timeSeconds: "asc" }],
|
||||||
|
take: 20,
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(results.map((r, i) => ({
|
||||||
|
rank: i + 1,
|
||||||
|
name: r.user.name,
|
||||||
|
clicks: r.clicks,
|
||||||
|
timeSeconds: r.timeSeconds,
|
||||||
|
userId: r.userId,
|
||||||
|
})));
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { auth } from "../../../auth";
|
||||||
|
import { prisma } from "../../../lib/prisma";
|
||||||
|
import { pickTwoArticles } from "../../../lib/wiki";
|
||||||
|
|
||||||
|
function todayKey() {
|
||||||
|
return new Date().toISOString().slice(0, 10); // "YYYY-MM-DD"
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getOrCreatePuzzle() {
|
||||||
|
const date = todayKey();
|
||||||
|
const existing = await prisma.dailyPuzzle.findUnique({ where: { date } });
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
const { start, target } = await pickTwoArticles();
|
||||||
|
return prisma.dailyPuzzle.create({ data: { date, startArticle: start, targetArticle: target } });
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET — retourne le puzzle du jour + si l'utilisateur a déjà joué
|
||||||
|
export async function GET() {
|
||||||
|
const session = await auth();
|
||||||
|
const puzzle = await getOrCreatePuzzle();
|
||||||
|
|
||||||
|
let myResult = null;
|
||||||
|
if (session?.user?.id) {
|
||||||
|
myResult = await prisma.dailyResult.findUnique({
|
||||||
|
where: { puzzleId_userId: { puzzleId: puzzle.id, userId: session.user.id } },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json({
|
||||||
|
puzzle: { id: puzzle.id, date: puzzle.date, startArticle: puzzle.startArticle, targetArticle: puzzle.targetArticle },
|
||||||
|
alreadyPlayed: !!myResult,
|
||||||
|
myResult: myResult ? { clicks: myResult.clicks, timeSeconds: myResult.timeSeconds, won: myResult.won, path: JSON.parse(myResult.path) } : null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST — soumettre un résultat
|
||||||
|
export async function POST(req: Request) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.id) return NextResponse.json({ error: "Non connecté" }, { status: 401 });
|
||||||
|
|
||||||
|
const { puzzleId, path, clicks, timeSeconds, won } = await req.json() as {
|
||||||
|
puzzleId: string; path: string[]; clicks: number; timeSeconds: number; won: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vérifier que le puzzle est bien celui du jour
|
||||||
|
const puzzle = await prisma.dailyPuzzle.findUnique({ where: { id: puzzleId } });
|
||||||
|
if (!puzzle || puzzle.date !== todayKey()) {
|
||||||
|
return NextResponse.json({ error: "Puzzle invalide" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert — on n'enregistre qu'une fois
|
||||||
|
const result = await prisma.dailyResult.upsert({
|
||||||
|
where: { puzzleId_userId: { puzzleId, userId: session.user.id } },
|
||||||
|
create: { puzzleId, userId: session.user.id, path: JSON.stringify(path), clicks, timeSeconds, won },
|
||||||
|
update: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json({ id: result.id });
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useBlitzGame } from "../../lib/useBlitzGame";
|
||||||
|
import { ArticleView } from "./ArticleView";
|
||||||
|
|
||||||
|
function fmtLeft(s: number): string {
|
||||||
|
const m = Math.floor(s / 60);
|
||||||
|
const sec = Math.floor(s % 60);
|
||||||
|
return `${m}:${String(sec).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BlitzScreen({ onBack }: { onBack: () => void }) {
|
||||||
|
const game = useBlitzGame();
|
||||||
|
|
||||||
|
const btnPrimary = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors";
|
||||||
|
const btnGhost = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors";
|
||||||
|
|
||||||
|
const danger = game.timeLeft < 30;
|
||||||
|
|
||||||
|
if (game.phase === "setup") return (
|
||||||
|
<div className="min-h-dvh bg-[#0f0f0f] text-[#f0f0f0] animate-fade-in flex flex-col items-center justify-center px-4 py-8 gap-5 max-w-sm mx-auto text-center">
|
||||||
|
<button className="self-start min-h-9 px-3 rounded-lg text-sm font-semibold bg-[#242424] border border-[#2e2e2e] hover:bg-[#1a1a1a] cursor-pointer" onClick={onBack}>
|
||||||
|
Retour
|
||||||
|
</button>
|
||||||
|
<div className="text-5xl">⚡</div>
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-black">Mode Blitz</h2>
|
||||||
|
<p className="text-[#888] text-sm sm:text-base leading-relaxed">
|
||||||
|
Tu as <span className="text-[#f0f0f0] font-bold">2 minutes</span> pour visiter un maximum d'articles Wikipedia différents en cliquant sur les liens. Ton score = nombre d'articles uniques visités.
|
||||||
|
</p>
|
||||||
|
<button className={btnPrimary} onClick={game.start} disabled={game.loading}>
|
||||||
|
{game.loading ? "Préparation..." : "Lancer le chrono !"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (game.phase === "ended") return (
|
||||||
|
<div className="min-h-dvh bg-[#0f0f0f] text-[#f0f0f0] animate-fade-in flex items-center justify-center px-4 py-8">
|
||||||
|
<div className="w-full max-w-sm sm:max-w-md text-center flex flex-col gap-5">
|
||||||
|
<div className="text-5xl sm:text-6xl leading-none">⚡</div>
|
||||||
|
<h2 className="text-2xl sm:text-3xl font-black">Temps écoulé !</h2>
|
||||||
|
<div className="flex gap-8 justify-center">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-4xl font-black text-[#7c3aed]">{game.visited.length}</span>
|
||||||
|
<span className="text-xs text-[#888]">articles visités</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-4xl font-black text-[#7c3aed]">{game.clicks}</span>
|
||||||
|
<span className="text-xs text-[#888]">clics</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-[#1a1a1a] rounded-xl px-4 py-3 flex flex-wrap gap-1 text-xs text-[#888] text-left max-h-48 overflow-y-auto">
|
||||||
|
{game.visited.map((t, i) => (
|
||||||
|
<span key={i} className="flex items-center gap-1">
|
||||||
|
{i > 0 && <span className="text-[#555]">›</span>}
|
||||||
|
<span>{t}</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2.5">
|
||||||
|
<button className={btnPrimary} onClick={game.start} disabled={game.loading}>
|
||||||
|
Rejouer
|
||||||
|
</button>
|
||||||
|
<button className={btnGhost} onClick={onBack}>Accueil</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Playing
|
||||||
|
return (
|
||||||
|
<div className="min-h-dvh w-full bg-[#0f0f0f] flex flex-col">
|
||||||
|
<div className="flex-1 overflow-y-auto bg-white">
|
||||||
|
<div className="article-container">
|
||||||
|
{game.title && <h1 className="article-title">{game.title}</h1>}
|
||||||
|
{game.loading && (
|
||||||
|
<div className="flex items-center gap-3 py-10 px-4 text-[#888]"><div className="spinner" /> Chargement...</div>
|
||||||
|
)}
|
||||||
|
{game.loadError && (
|
||||||
|
<div className="flex flex-col items-center gap-4 py-10 px-4 text-center">
|
||||||
|
<p className="text-[#888] text-sm">{game.loadError}</p>
|
||||||
|
<button className="min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#2563eb] text-white cursor-pointer" onClick={() => game.retryLoad()}>
|
||||||
|
Réessayer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!game.loading && !game.loadError && game.html && (
|
||||||
|
<ArticleView html={game.html} onNavigate={game.navigate} disabled={game.loading} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom bar */}
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 z-50 bg-[#0f0f0f]/97 backdrop-blur-sm border-t border-[#2e2e2e] flex items-center justify-between px-3 sm:px-4 py-2 gap-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex flex-col items-center gap-0.5">
|
||||||
|
<span className="text-[8px] font-bold uppercase tracking-wider text-[#888]">⚡ Articles</span>
|
||||||
|
<span className="text-sm font-black text-[#7c3aed] tabular-nums">{game.visited.length}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col items-center gap-0.5">
|
||||||
|
<span className="text-[8px] font-bold uppercase tracking-wider text-[#888]">Clics</span>
|
||||||
|
<span className="text-sm font-black tabular-nums">{game.clicks}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timer */}
|
||||||
|
<div className={`text-2xl sm:text-3xl font-black tabular-nums transition-colors ${danger ? "text-red-400" : "text-[#f0f0f0]"}`}>
|
||||||
|
{fmtLeft(game.timeLeft)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-xs text-[#888] text-right max-w-28 truncate">
|
||||||
|
{game.visited.length > 0 ? game.visited[game.visited.length - 1] : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useDailyGame } from "../../lib/useDailyGame";
|
||||||
|
import { fmt } from "../../lib/utils";
|
||||||
|
import { ArticleView } from "./ArticleView";
|
||||||
|
import { Breadcrumbs } from "./Breadcrumbs";
|
||||||
|
|
||||||
|
const MEDALS = ["🥇", "🥈", "🥉"];
|
||||||
|
|
||||||
|
type LeaderboardEntry = { rank: number; name: string; clicks: number; timeSeconds: number; userId: string };
|
||||||
|
|
||||||
|
export function DailyScreen({ onBack, currentUserId }: { onBack: () => void; currentUserId?: string }) {
|
||||||
|
const game = useDailyGame();
|
||||||
|
const breadcrumbEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [leaderboard, setLeaderboard] = useState<LeaderboardEntry[]>([]);
|
||||||
|
const [loadingLb, setLoadingLb] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
breadcrumbEndRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "end" });
|
||||||
|
}, [game.history]);
|
||||||
|
|
||||||
|
function fetchLeaderboard() {
|
||||||
|
setLoadingLb(true);
|
||||||
|
fetch("/api/daily/leaderboard")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then(setLeaderboard)
|
||||||
|
.finally(() => setLoadingLb(false));
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (game.phase === "won" || game.phase === "gave_up" || game.phase === "already_played") {
|
||||||
|
fetchLeaderboard();
|
||||||
|
}
|
||||||
|
}, [game.phase]);
|
||||||
|
|
||||||
|
const btnPrimary = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#7c3aed] text-white hover:bg-[#6d28d9] disabled:opacity-50 cursor-pointer transition-colors";
|
||||||
|
const btnGhost = "w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors";
|
||||||
|
|
||||||
|
// Loading initial
|
||||||
|
if (game.phase === "loading") return (
|
||||||
|
<div className="min-h-dvh bg-[#0f0f0f] flex items-center justify-center">
|
||||||
|
<div className="spinner" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Erreur initiale
|
||||||
|
if (game.loadError && game.phase === "loading") return (
|
||||||
|
<div className="min-h-dvh bg-[#0f0f0f] text-[#f0f0f0] flex flex-col items-center justify-center gap-4 px-4">
|
||||||
|
<p className="text-[#888]">{game.loadError}</p>
|
||||||
|
<button className={btnGhost} style={{ width: "auto", padding: "0 20px" }} onClick={onBack}>Retour</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Résultat (won / gave_up / already_played)
|
||||||
|
if (game.phase === "won" || game.phase === "gave_up" || game.phase === "already_played") {
|
||||||
|
const result = game.myResult;
|
||||||
|
return (
|
||||||
|
<div className="min-h-dvh bg-[#0f0f0f] text-[#f0f0f0] animate-fade-in flex flex-col max-w-lg mx-auto px-4 py-8 gap-6">
|
||||||
|
<button className="self-start min-h-9 px-3 rounded-lg text-sm font-semibold bg-[#242424] border border-[#2e2e2e] hover:bg-[#1a1a1a] cursor-pointer" onClick={onBack}>
|
||||||
|
← Retour
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-xs font-bold text-[#888] uppercase tracking-widest mb-1">Défi du jour — {game.puzzle?.date}</div>
|
||||||
|
<div className="text-2xl sm:text-3xl font-black mb-1">
|
||||||
|
{game.phase === "won" ? "🎉 Réussi !" : game.phase === "gave_up" ? "😔 Abandonné" : "✅ Déjà joué"}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-[#888]">
|
||||||
|
<span className="text-[#f0f0f0] font-semibold">{game.puzzle?.startArticle}</span>
|
||||||
|
<span className="mx-2">→</span>
|
||||||
|
<span className="text-[#7c3aed] font-semibold">{game.puzzle?.targetArticle}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{result && (
|
||||||
|
<div className="bg-[#1a1a1a] border border-[#2e2e2e] rounded-xl p-4 flex flex-col gap-3">
|
||||||
|
<div className="flex justify-center gap-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-3xl font-black text-[#7c3aed]">{result.clicks}</div>
|
||||||
|
<div className="text-xs text-[#888]">clics</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<div className="text-3xl font-black text-[#7c3aed]">{fmt(result.timeSeconds)}</div>
|
||||||
|
<div className="text-xs text-[#888]">temps</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{result.path.length > 0 && (
|
||||||
|
<div className="flex flex-wrap gap-1 text-xs text-[#888] pt-3 border-t border-[#2e2e2e]">
|
||||||
|
{result.path.map((t, i) => (
|
||||||
|
<span key={i} className="flex items-center gap-0.5">
|
||||||
|
{i > 0 && <span className="text-[#555]">›</span>}
|
||||||
|
<span className={i === result.path.length - 1 && result.won ? "text-[#16a34a] font-semibold" : ""}>{t}</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Classement du jour */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xs font-bold text-[#888] uppercase tracking-wider mb-3">Classement du jour</h3>
|
||||||
|
{loadingLb ? (
|
||||||
|
<div className="flex justify-center py-6"><div className="spinner" /></div>
|
||||||
|
) : leaderboard.length === 0 ? (
|
||||||
|
<p className="text-sm text-[#888] text-center py-4">Aucun résultat pour l'instant.</p>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{leaderboard.map((entry, i) => (
|
||||||
|
<div key={i} className={`flex items-center gap-3 px-3.5 py-2.5 rounded-xl border ${entry.userId === currentUserId ? "border-[#7c3aed] bg-[#7c3aed]/8" : "border-[#2e2e2e] bg-[#1a1a1a]"}`}>
|
||||||
|
<span className="text-lg w-7 text-center shrink-0">{MEDALS[i] ?? `#${entry.rank}`}</span>
|
||||||
|
<span className="flex-1 font-semibold text-sm truncate">{entry.name}</span>
|
||||||
|
<span className="text-xs text-[#888] tabular-nums shrink-0">{entry.clicks} clics · {fmt(entry.timeSeconds)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className={btnGhost} onClick={onBack}>Retour à l'accueil</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Playing
|
||||||
|
return (
|
||||||
|
<div className="min-h-dvh w-full bg-[#0f0f0f] flex flex-col">
|
||||||
|
<div className="flex-1 overflow-y-auto bg-white">
|
||||||
|
<div className="article-container">
|
||||||
|
{game.title && <h1 className="article-title">{game.title}</h1>}
|
||||||
|
{game.loading && (
|
||||||
|
<div className="flex items-center gap-3 py-10 px-4 text-[#888]"><div className="spinner" /> Chargement...</div>
|
||||||
|
)}
|
||||||
|
{game.loadError && (
|
||||||
|
<div className="flex flex-col items-center gap-4 py-10 px-4 text-center">
|
||||||
|
<p className="text-[#888] text-sm">{game.loadError}</p>
|
||||||
|
<button className="min-h-11 px-5 rounded-xl text-sm font-semibold bg-[#2563eb] text-white cursor-pointer" onClick={() => game.retryLoad()}>
|
||||||
|
Réessayer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!game.loading && !game.loadError && game.html && (
|
||||||
|
<ArticleView html={game.html} onNavigate={game.navigate} disabled={game.loading} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom bar */}
|
||||||
|
<div className="fixed bottom-0 left-0 right-0 z-50 bg-[#0f0f0f]/97 backdrop-blur-sm border-t border-[#2e2e2e] flex items-center justify-between px-3 sm:px-4 py-2 gap-2">
|
||||||
|
<div className="flex-1 min-w-0 flex flex-col gap-0.5">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<span className="text-[8px] font-bold uppercase tracking-widest text-[#888] shrink-0">🗓 Défi du jour · cible</span>
|
||||||
|
<span className="text-xs font-black text-[#7c3aed] truncate">{game.puzzle?.targetArticle}</span>
|
||||||
|
</div>
|
||||||
|
<Breadcrumbs history={game.history} endRef={breadcrumbEndRef} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 shrink-0">
|
||||||
|
<div className="flex flex-col items-center gap-0.5 min-w-10">
|
||||||
|
<span className="text-[8px] font-bold tracking-wider text-[#888] uppercase">Clics</span>
|
||||||
|
<span className="text-xs font-black tabular-nums">{game.clicks}</span>
|
||||||
|
</div>
|
||||||
|
{game.canGoBack && (
|
||||||
|
<button className="min-h-8 px-2 rounded-lg text-xs font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] disabled:opacity-50 cursor-pointer" onClick={game.goBack} disabled={game.loading}>
|
||||||
|
← +1
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button className="min-h-8 px-2 rounded-lg text-xs font-semibold bg-red-950/40 border border-red-900 text-red-400 hover:bg-red-900/40 cursor-pointer" onClick={game.giveUp}>
|
||||||
|
Abandonner
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,12 +18,14 @@ type HomeScreenProps = {
|
|||||||
onShowAuth: () => void;
|
onShowAuth: () => void;
|
||||||
onShowProfile: () => void;
|
onShowProfile: () => void;
|
||||||
onShowLeaderboard: () => void;
|
onShowLeaderboard: () => void;
|
||||||
|
onDaily: () => void;
|
||||||
|
onBlitz: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function HomeScreen({
|
export function HomeScreen({
|
||||||
playerName, setPlayerName, joinCode, setJoinCode,
|
playerName, setPlayerName, joinCode, setJoinCode,
|
||||||
error, setError, loading, onCreateRoom, onJoinRoom, onSolo,
|
error, setError, loading, onCreateRoom, onJoinRoom, onSolo,
|
||||||
session, onShowAuth, onShowProfile, onShowLeaderboard,
|
session, onShowAuth, onShowProfile, onShowLeaderboard, onDaily, onBlitz,
|
||||||
}: HomeScreenProps) {
|
}: HomeScreenProps) {
|
||||||
const btnGhost = "min-h-9 px-3 rounded-lg text-xs sm:text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer whitespace-nowrap";
|
const btnGhost = "min-h-9 px-3 rounded-lg text-xs sm:text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer whitespace-nowrap";
|
||||||
|
|
||||||
@@ -114,7 +116,19 @@ export function HomeScreen({
|
|||||||
className="w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"
|
className="w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"
|
||||||
onClick={onSolo}
|
onClick={onSolo}
|
||||||
>
|
>
|
||||||
Jouer en solo
|
🎯 Jouer en solo
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"
|
||||||
|
onClick={onDaily}
|
||||||
|
>
|
||||||
|
🗓 Défi du jour
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="w-full min-h-11 rounded-xl text-sm font-semibold bg-[#242424] border border-[#2e2e2e] text-[#f0f0f0] hover:bg-[#1a1a1a] cursor-pointer transition-colors"
|
||||||
|
onClick={onBlitz}
|
||||||
|
>
|
||||||
|
⚡ Mode Blitz — 2 min
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { GameScreen } from "./GameScreen";
|
|||||||
import { AuthModal } from "./AuthModal";
|
import { AuthModal } from "./AuthModal";
|
||||||
import { ProfileScreen } from "./ProfileScreen";
|
import { ProfileScreen } from "./ProfileScreen";
|
||||||
import { LeaderboardScreen } from "./LeaderboardScreen";
|
import { LeaderboardScreen } from "./LeaderboardScreen";
|
||||||
|
import { DailyScreen } from "./DailyScreen";
|
||||||
|
import { BlitzScreen } from "./BlitzScreen";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
screen: Screen;
|
screen: Screen;
|
||||||
@@ -46,6 +48,14 @@ export function ScreenRouter({
|
|||||||
<LeaderboardScreen onBack={() => setScreen("home")} />
|
<LeaderboardScreen onBack={() => setScreen("home")} />
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (screen === "daily") return (
|
||||||
|
<DailyScreen onBack={() => setScreen("home")} currentUserId={session?.user?.id ?? undefined} />
|
||||||
|
);
|
||||||
|
|
||||||
|
if (screen === "blitz") return (
|
||||||
|
<BlitzScreen onBack={() => setScreen("home")} />
|
||||||
|
);
|
||||||
|
|
||||||
if (screen === "home") return (
|
if (screen === "home") return (
|
||||||
<>
|
<>
|
||||||
<HomeScreen
|
<HomeScreen
|
||||||
@@ -59,6 +69,8 @@ export function ScreenRouter({
|
|||||||
onShowAuth={() => setShowAuth(true)}
|
onShowAuth={() => setShowAuth(true)}
|
||||||
onShowProfile={() => setScreen("profile")}
|
onShowProfile={() => setScreen("profile")}
|
||||||
onShowLeaderboard={() => setScreen("leaderboard")}
|
onShowLeaderboard={() => setScreen("leaderboard")}
|
||||||
|
onDaily={() => setScreen("daily")}
|
||||||
|
onBlitz={() => setScreen("blitz")}
|
||||||
/>
|
/>
|
||||||
{showAuth && <AuthModal onClose={() => setShowAuth(false)} onSuccess={() => setShowAuth(false)} />}
|
{showAuth && <AuthModal onClose={() => setShowAuth(false)} onSuccess={() => setShowAuth(false)} />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -27,3 +27,13 @@ export type User = Prisma.UserModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Game = Prisma.GameModel
|
export type Game = Prisma.GameModel
|
||||||
|
/**
|
||||||
|
* Model DailyPuzzle
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type DailyPuzzle = Prisma.DailyPuzzleModel
|
||||||
|
/**
|
||||||
|
* Model DailyResult
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type DailyResult = Prisma.DailyResultModel
|
||||||
|
|||||||
@@ -51,3 +51,13 @@ export type User = Prisma.UserModel
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Game = Prisma.GameModel
|
export type Game = Prisma.GameModel
|
||||||
|
/**
|
||||||
|
* Model DailyPuzzle
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type DailyPuzzle = Prisma.DailyPuzzleModel
|
||||||
|
/**
|
||||||
|
* Model DailyResult
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
export type DailyResult = Prisma.DailyResultModel
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -385,7 +385,9 @@ type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRe
|
|||||||
|
|
||||||
export const ModelName = {
|
export const ModelName = {
|
||||||
User: 'User',
|
User: 'User',
|
||||||
Game: 'Game'
|
Game: 'Game',
|
||||||
|
DailyPuzzle: 'DailyPuzzle',
|
||||||
|
DailyResult: 'DailyResult'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
@@ -401,7 +403,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
omit: GlobalOmitOptions
|
omit: GlobalOmitOptions
|
||||||
}
|
}
|
||||||
meta: {
|
meta: {
|
||||||
modelProps: "user" | "game"
|
modelProps: "user" | "game" | "dailyPuzzle" | "dailyResult"
|
||||||
txIsolationLevel: TransactionIsolationLevel
|
txIsolationLevel: TransactionIsolationLevel
|
||||||
}
|
}
|
||||||
model: {
|
model: {
|
||||||
@@ -553,6 +555,154 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DailyPuzzle: {
|
||||||
|
payload: Prisma.$DailyPuzzlePayload<ExtArgs>
|
||||||
|
fields: Prisma.DailyPuzzleFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.DailyPuzzleFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyPuzzlePayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.DailyPuzzleFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyPuzzlePayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.DailyPuzzleFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyPuzzlePayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.DailyPuzzleFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyPuzzlePayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.DailyPuzzleFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyPuzzlePayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.DailyPuzzleCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyPuzzlePayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.DailyPuzzleCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
createManyAndReturn: {
|
||||||
|
args: Prisma.DailyPuzzleCreateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyPuzzlePayload>[]
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.DailyPuzzleDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyPuzzlePayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.DailyPuzzleUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyPuzzlePayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.DailyPuzzleDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.DailyPuzzleUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateManyAndReturn: {
|
||||||
|
args: Prisma.DailyPuzzleUpdateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyPuzzlePayload>[]
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.DailyPuzzleUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyPuzzlePayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.DailyPuzzleAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateDailyPuzzle>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.DailyPuzzleGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.DailyPuzzleGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.DailyPuzzleCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.DailyPuzzleCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DailyResult: {
|
||||||
|
payload: Prisma.$DailyResultPayload<ExtArgs>
|
||||||
|
fields: Prisma.DailyResultFieldRefs
|
||||||
|
operations: {
|
||||||
|
findUnique: {
|
||||||
|
args: Prisma.DailyResultFindUniqueArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyResultPayload> | null
|
||||||
|
}
|
||||||
|
findUniqueOrThrow: {
|
||||||
|
args: Prisma.DailyResultFindUniqueOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyResultPayload>
|
||||||
|
}
|
||||||
|
findFirst: {
|
||||||
|
args: Prisma.DailyResultFindFirstArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyResultPayload> | null
|
||||||
|
}
|
||||||
|
findFirstOrThrow: {
|
||||||
|
args: Prisma.DailyResultFindFirstOrThrowArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyResultPayload>
|
||||||
|
}
|
||||||
|
findMany: {
|
||||||
|
args: Prisma.DailyResultFindManyArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyResultPayload>[]
|
||||||
|
}
|
||||||
|
create: {
|
||||||
|
args: Prisma.DailyResultCreateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyResultPayload>
|
||||||
|
}
|
||||||
|
createMany: {
|
||||||
|
args: Prisma.DailyResultCreateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
createManyAndReturn: {
|
||||||
|
args: Prisma.DailyResultCreateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyResultPayload>[]
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
args: Prisma.DailyResultDeleteArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyResultPayload>
|
||||||
|
}
|
||||||
|
update: {
|
||||||
|
args: Prisma.DailyResultUpdateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyResultPayload>
|
||||||
|
}
|
||||||
|
deleteMany: {
|
||||||
|
args: Prisma.DailyResultDeleteManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateMany: {
|
||||||
|
args: Prisma.DailyResultUpdateManyArgs<ExtArgs>
|
||||||
|
result: BatchPayload
|
||||||
|
}
|
||||||
|
updateManyAndReturn: {
|
||||||
|
args: Prisma.DailyResultUpdateManyAndReturnArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyResultPayload>[]
|
||||||
|
}
|
||||||
|
upsert: {
|
||||||
|
args: Prisma.DailyResultUpsertArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.PayloadToResult<Prisma.$DailyResultPayload>
|
||||||
|
}
|
||||||
|
aggregate: {
|
||||||
|
args: Prisma.DailyResultAggregateArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.AggregateDailyResult>
|
||||||
|
}
|
||||||
|
groupBy: {
|
||||||
|
args: Prisma.DailyResultGroupByArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.DailyResultGroupByOutputType>[]
|
||||||
|
}
|
||||||
|
count: {
|
||||||
|
args: Prisma.DailyResultCountArgs<ExtArgs>
|
||||||
|
result: runtime.Types.Utils.Optional<Prisma.DailyResultCountAggregateOutputType> | number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} & {
|
} & {
|
||||||
other: {
|
other: {
|
||||||
@@ -616,6 +766,30 @@ export const GameScalarFieldEnum = {
|
|||||||
export type GameScalarFieldEnum = (typeof GameScalarFieldEnum)[keyof typeof GameScalarFieldEnum]
|
export type GameScalarFieldEnum = (typeof GameScalarFieldEnum)[keyof typeof GameScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const DailyPuzzleScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
date: 'date',
|
||||||
|
startArticle: 'startArticle',
|
||||||
|
targetArticle: 'targetArticle'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type DailyPuzzleScalarFieldEnum = (typeof DailyPuzzleScalarFieldEnum)[keyof typeof DailyPuzzleScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const DailyResultScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
puzzleId: 'puzzleId',
|
||||||
|
userId: 'userId',
|
||||||
|
path: 'path',
|
||||||
|
clicks: 'clicks',
|
||||||
|
timeSeconds: 'timeSeconds',
|
||||||
|
won: 'won',
|
||||||
|
playedAt: 'playedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type DailyResultScalarFieldEnum = (typeof DailyResultScalarFieldEnum)[keyof typeof DailyResultScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -761,6 +935,8 @@ export type PrismaClientOptions = ({
|
|||||||
export type GlobalOmitConfig = {
|
export type GlobalOmitConfig = {
|
||||||
user?: Prisma.UserOmit
|
user?: Prisma.UserOmit
|
||||||
game?: Prisma.GameOmit
|
game?: Prisma.GameOmit
|
||||||
|
dailyPuzzle?: Prisma.DailyPuzzleOmit
|
||||||
|
dailyResult?: Prisma.DailyResultOmit
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Types for Logging */
|
/* Types for Logging */
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ export const AnyNull = runtime.AnyNull
|
|||||||
|
|
||||||
export const ModelName = {
|
export const ModelName = {
|
||||||
User: 'User',
|
User: 'User',
|
||||||
Game: 'Game'
|
Game: 'Game',
|
||||||
|
DailyPuzzle: 'DailyPuzzle',
|
||||||
|
DailyResult: 'DailyResult'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||||
@@ -95,6 +97,30 @@ export const GameScalarFieldEnum = {
|
|||||||
export type GameScalarFieldEnum = (typeof GameScalarFieldEnum)[keyof typeof GameScalarFieldEnum]
|
export type GameScalarFieldEnum = (typeof GameScalarFieldEnum)[keyof typeof GameScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const DailyPuzzleScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
date: 'date',
|
||||||
|
startArticle: 'startArticle',
|
||||||
|
targetArticle: 'targetArticle'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type DailyPuzzleScalarFieldEnum = (typeof DailyPuzzleScalarFieldEnum)[keyof typeof DailyPuzzleScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
|
export const DailyResultScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
puzzleId: 'puzzleId',
|
||||||
|
userId: 'userId',
|
||||||
|
path: 'path',
|
||||||
|
clicks: 'clicks',
|
||||||
|
timeSeconds: 'timeSeconds',
|
||||||
|
won: 'won',
|
||||||
|
playedAt: 'playedAt'
|
||||||
|
} as const
|
||||||
|
|
||||||
|
export type DailyResultScalarFieldEnum = (typeof DailyResultScalarFieldEnum)[keyof typeof DailyResultScalarFieldEnum]
|
||||||
|
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
|
|||||||
@@ -10,4 +10,6 @@
|
|||||||
*/
|
*/
|
||||||
export type * from './models/User'
|
export type * from './models/User'
|
||||||
export type * from './models/Game'
|
export type * from './models/Game'
|
||||||
|
export type * from './models/DailyPuzzle'
|
||||||
|
export type * from './models/DailyResult'
|
||||||
export type * from './commonInputTypes'
|
export type * from './commonInputTypes'
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -183,6 +183,7 @@ export type UserWhereInput = {
|
|||||||
password?: Prisma.StringFilter<"User"> | string
|
password?: Prisma.StringFilter<"User"> | string
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
games?: Prisma.GameListRelationFilter
|
games?: Prisma.GameListRelationFilter
|
||||||
|
dailyResults?: Prisma.DailyResultListRelationFilter
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserOrderByWithRelationInput = {
|
export type UserOrderByWithRelationInput = {
|
||||||
@@ -192,6 +193,7 @@ export type UserOrderByWithRelationInput = {
|
|||||||
password?: Prisma.SortOrder
|
password?: Prisma.SortOrder
|
||||||
createdAt?: Prisma.SortOrder
|
createdAt?: Prisma.SortOrder
|
||||||
games?: Prisma.GameOrderByRelationAggregateInput
|
games?: Prisma.GameOrderByRelationAggregateInput
|
||||||
|
dailyResults?: Prisma.DailyResultOrderByRelationAggregateInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserWhereUniqueInput = Prisma.AtLeast<{
|
export type UserWhereUniqueInput = Prisma.AtLeast<{
|
||||||
@@ -204,6 +206,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
|
|||||||
password?: Prisma.StringFilter<"User"> | string
|
password?: Prisma.StringFilter<"User"> | string
|
||||||
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
|
||||||
games?: Prisma.GameListRelationFilter
|
games?: Prisma.GameListRelationFilter
|
||||||
|
dailyResults?: Prisma.DailyResultListRelationFilter
|
||||||
}, "id" | "email">
|
}, "id" | "email">
|
||||||
|
|
||||||
export type UserOrderByWithAggregationInput = {
|
export type UserOrderByWithAggregationInput = {
|
||||||
@@ -235,6 +238,7 @@ export type UserCreateInput = {
|
|||||||
password: string
|
password: string
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
games?: Prisma.GameCreateNestedManyWithoutUserInput
|
games?: Prisma.GameCreateNestedManyWithoutUserInput
|
||||||
|
dailyResults?: Prisma.DailyResultCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedCreateInput = {
|
export type UserUncheckedCreateInput = {
|
||||||
@@ -244,6 +248,7 @@ export type UserUncheckedCreateInput = {
|
|||||||
password: string
|
password: string
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
games?: Prisma.GameUncheckedCreateNestedManyWithoutUserInput
|
games?: Prisma.GameUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
dailyResults?: Prisma.DailyResultUncheckedCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUpdateInput = {
|
export type UserUpdateInput = {
|
||||||
@@ -253,6 +258,7 @@ export type UserUpdateInput = {
|
|||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
games?: Prisma.GameUpdateManyWithoutUserNestedInput
|
games?: Prisma.GameUpdateManyWithoutUserNestedInput
|
||||||
|
dailyResults?: Prisma.DailyResultUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateInput = {
|
export type UserUncheckedUpdateInput = {
|
||||||
@@ -262,6 +268,7 @@ export type UserUncheckedUpdateInput = {
|
|||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
games?: Prisma.GameUncheckedUpdateManyWithoutUserNestedInput
|
games?: Prisma.GameUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
dailyResults?: Prisma.DailyResultUncheckedUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateManyInput = {
|
export type UserCreateManyInput = {
|
||||||
@@ -339,12 +346,27 @@ export type UserUpdateOneRequiredWithoutGamesNestedInput = {
|
|||||||
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutGamesInput, Prisma.UserUpdateWithoutGamesInput>, Prisma.UserUncheckedUpdateWithoutGamesInput>
|
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutGamesInput, Prisma.UserUpdateWithoutGamesInput>, Prisma.UserUncheckedUpdateWithoutGamesInput>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type UserCreateNestedOneWithoutDailyResultsInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutDailyResultsInput, Prisma.UserUncheckedCreateWithoutDailyResultsInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutDailyResultsInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateOneRequiredWithoutDailyResultsNestedInput = {
|
||||||
|
create?: Prisma.XOR<Prisma.UserCreateWithoutDailyResultsInput, Prisma.UserUncheckedCreateWithoutDailyResultsInput>
|
||||||
|
connectOrCreate?: Prisma.UserCreateOrConnectWithoutDailyResultsInput
|
||||||
|
upsert?: Prisma.UserUpsertWithoutDailyResultsInput
|
||||||
|
connect?: Prisma.UserWhereUniqueInput
|
||||||
|
update?: Prisma.XOR<Prisma.XOR<Prisma.UserUpdateToOneWithWhereWithoutDailyResultsInput, Prisma.UserUpdateWithoutDailyResultsInput>, Prisma.UserUncheckedUpdateWithoutDailyResultsInput>
|
||||||
|
}
|
||||||
|
|
||||||
export type UserCreateWithoutGamesInput = {
|
export type UserCreateWithoutGamesInput = {
|
||||||
id?: string
|
id?: string
|
||||||
name: string
|
name: string
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
dailyResults?: Prisma.DailyResultCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedCreateWithoutGamesInput = {
|
export type UserUncheckedCreateWithoutGamesInput = {
|
||||||
@@ -353,6 +375,7 @@ export type UserUncheckedCreateWithoutGamesInput = {
|
|||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
createdAt?: Date | string
|
createdAt?: Date | string
|
||||||
|
dailyResults?: Prisma.DailyResultUncheckedCreateNestedManyWithoutUserInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCreateOrConnectWithoutGamesInput = {
|
export type UserCreateOrConnectWithoutGamesInput = {
|
||||||
@@ -377,6 +400,7 @@ export type UserUpdateWithoutGamesInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
dailyResults?: Prisma.DailyResultUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserUncheckedUpdateWithoutGamesInput = {
|
export type UserUncheckedUpdateWithoutGamesInput = {
|
||||||
@@ -385,6 +409,59 @@ export type UserUncheckedUpdateWithoutGamesInput = {
|
|||||||
email?: Prisma.StringFieldUpdateOperationsInput | string
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
password?: Prisma.StringFieldUpdateOperationsInput | string
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
dailyResults?: Prisma.DailyResultUncheckedUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateWithoutDailyResultsInput = {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
games?: Prisma.GameCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedCreateWithoutDailyResultsInput = {
|
||||||
|
id?: string
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
createdAt?: Date | string
|
||||||
|
games?: Prisma.GameUncheckedCreateNestedManyWithoutUserInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserCreateOrConnectWithoutDailyResultsInput = {
|
||||||
|
where: Prisma.UserWhereUniqueInput
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutDailyResultsInput, Prisma.UserUncheckedCreateWithoutDailyResultsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpsertWithoutDailyResultsInput = {
|
||||||
|
update: Prisma.XOR<Prisma.UserUpdateWithoutDailyResultsInput, Prisma.UserUncheckedUpdateWithoutDailyResultsInput>
|
||||||
|
create: Prisma.XOR<Prisma.UserCreateWithoutDailyResultsInput, Prisma.UserUncheckedCreateWithoutDailyResultsInput>
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateToOneWithWhereWithoutDailyResultsInput = {
|
||||||
|
where?: Prisma.UserWhereInput
|
||||||
|
data: Prisma.XOR<Prisma.UserUpdateWithoutDailyResultsInput, Prisma.UserUncheckedUpdateWithoutDailyResultsInput>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUpdateWithoutDailyResultsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
games?: Prisma.GameUpdateManyWithoutUserNestedInput
|
||||||
|
}
|
||||||
|
|
||||||
|
export type UserUncheckedUpdateWithoutDailyResultsInput = {
|
||||||
|
id?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
name?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
email?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
password?: Prisma.StringFieldUpdateOperationsInput | string
|
||||||
|
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||||
|
games?: Prisma.GameUncheckedUpdateManyWithoutUserNestedInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -394,10 +471,12 @@ export type UserUncheckedUpdateWithoutGamesInput = {
|
|||||||
|
|
||||||
export type UserCountOutputType = {
|
export type UserCountOutputType = {
|
||||||
games: number
|
games: number
|
||||||
|
dailyResults: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
games?: boolean | UserCountOutputTypeCountGamesArgs
|
games?: boolean | UserCountOutputTypeCountGamesArgs
|
||||||
|
dailyResults?: boolean | UserCountOutputTypeCountDailyResultsArgs
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -417,6 +496,13 @@ export type UserCountOutputTypeCountGamesArgs<ExtArgs extends runtime.Types.Exte
|
|||||||
where?: Prisma.GameWhereInput
|
where?: Prisma.GameWhereInput
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UserCountOutputType without action
|
||||||
|
*/
|
||||||
|
export type UserCountOutputTypeCountDailyResultsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
where?: Prisma.DailyResultWhereInput
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetSelect<{
|
||||||
id?: boolean
|
id?: boolean
|
||||||
@@ -425,6 +511,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
|
|||||||
password?: boolean
|
password?: boolean
|
||||||
createdAt?: boolean
|
createdAt?: boolean
|
||||||
games?: boolean | Prisma.User$gamesArgs<ExtArgs>
|
games?: boolean | Prisma.User$gamesArgs<ExtArgs>
|
||||||
|
dailyResults?: boolean | Prisma.User$dailyResultsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}, ExtArgs["result"]["user"]>
|
}, ExtArgs["result"]["user"]>
|
||||||
|
|
||||||
@@ -455,6 +542,7 @@ export type UserSelectScalar = {
|
|||||||
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "password" | "createdAt", ExtArgs["result"]["user"]>
|
export type UserOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "name" | "email" | "password" | "createdAt", ExtArgs["result"]["user"]>
|
||||||
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
export type UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
games?: boolean | Prisma.User$gamesArgs<ExtArgs>
|
games?: boolean | Prisma.User$gamesArgs<ExtArgs>
|
||||||
|
dailyResults?: boolean | Prisma.User$dailyResultsArgs<ExtArgs>
|
||||||
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
|
||||||
}
|
}
|
||||||
export type UserIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
export type UserIncludeCreateManyAndReturn<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {}
|
||||||
@@ -464,6 +552,7 @@ export type $UserPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
|||||||
name: "User"
|
name: "User"
|
||||||
objects: {
|
objects: {
|
||||||
games: Prisma.$GamePayload<ExtArgs>[]
|
games: Prisma.$GamePayload<ExtArgs>[]
|
||||||
|
dailyResults: Prisma.$DailyResultPayload<ExtArgs>[]
|
||||||
}
|
}
|
||||||
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
scalars: runtime.Types.Extensions.GetPayloadResult<{
|
||||||
id: string
|
id: string
|
||||||
@@ -866,6 +955,7 @@ readonly fields: UserFieldRefs;
|
|||||||
export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
export interface Prisma__UserClient<T, Null = never, ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> extends Prisma.PrismaPromise<T> {
|
||||||
readonly [Symbol.toStringTag]: "PrismaPromise"
|
readonly [Symbol.toStringTag]: "PrismaPromise"
|
||||||
games<T extends Prisma.User$gamesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$gamesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$GamePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
games<T extends Prisma.User$gamesArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$gamesArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$GamePayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
|
dailyResults<T extends Prisma.User$dailyResultsArgs<ExtArgs> = {}>(args?: Prisma.Subset<T, Prisma.User$dailyResultsArgs<ExtArgs>>): Prisma.PrismaPromise<runtime.Types.Result.GetResult<Prisma.$DailyResultPayload<ExtArgs>, T, "findMany", GlobalOmitOptions> | Null>
|
||||||
/**
|
/**
|
||||||
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
* Attaches callbacks for the resolution and/or rejection of the Promise.
|
||||||
* @param onfulfilled The callback to execute when the Promise is resolved.
|
* @param onfulfilled The callback to execute when the Promise is resolved.
|
||||||
@@ -1314,6 +1404,30 @@ export type User$gamesArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs
|
|||||||
distinct?: Prisma.GameScalarFieldEnum | Prisma.GameScalarFieldEnum[]
|
distinct?: Prisma.GameScalarFieldEnum | Prisma.GameScalarFieldEnum[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User.dailyResults
|
||||||
|
*/
|
||||||
|
export type User$dailyResultsArgs<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||||
|
/**
|
||||||
|
* Select specific fields to fetch from the DailyResult
|
||||||
|
*/
|
||||||
|
select?: Prisma.DailyResultSelect<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Omit specific fields from the DailyResult
|
||||||
|
*/
|
||||||
|
omit?: Prisma.DailyResultOmit<ExtArgs> | null
|
||||||
|
/**
|
||||||
|
* Choose, which related nodes to fetch as well
|
||||||
|
*/
|
||||||
|
include?: Prisma.DailyResultInclude<ExtArgs> | null
|
||||||
|
where?: Prisma.DailyResultWhereInput
|
||||||
|
orderBy?: Prisma.DailyResultOrderByWithRelationInput | Prisma.DailyResultOrderByWithRelationInput[]
|
||||||
|
cursor?: Prisma.DailyResultWhereUniqueInput
|
||||||
|
take?: number
|
||||||
|
skip?: number
|
||||||
|
distinct?: Prisma.DailyResultScalarFieldEnum | Prisma.DailyResultScalarFieldEnum[]
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* User without action
|
* User without action
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
export type Screen = "home" | "lobby" | "game" | "solo" | "profile" | "leaderboard";
|
export type Screen = "home" | "lobby" | "game" | "solo" | "profile" | "leaderboard" | "daily" | "blitz";
|
||||||
|
|
||||||
export type WikiArticle = {
|
export type WikiArticle = {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useCallback, useRef, useEffect } from "react";
|
||||||
|
import { fetchArticle, pickTwoArticles } from "./wiki";
|
||||||
|
|
||||||
|
const BLITZ_DURATION = 120; // 2 minutes en secondes
|
||||||
|
|
||||||
|
export type BlitzPhase = "setup" | "playing" | "ended";
|
||||||
|
|
||||||
|
export function useBlitzGame() {
|
||||||
|
const [phase, setPhase] = useState<BlitzPhase>("setup");
|
||||||
|
const [html, setHtml] = useState("");
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [timeLeft, setTimeLeft] = useState(BLITZ_DURATION);
|
||||||
|
const [clicks, setClicks] = useState(0);
|
||||||
|
const [visited, setVisited] = useState<string[]>([]); // articles uniques
|
||||||
|
|
||||||
|
const clicksRef = useRef(0);
|
||||||
|
const visitedRef = useRef<Set<string>>(new Set());
|
||||||
|
const visitedListRef = useRef<string[]>([]);
|
||||||
|
const loadingRef = useRef(false);
|
||||||
|
const endedRef = useRef(false);
|
||||||
|
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
const startTimeRef = useRef<number>(0);
|
||||||
|
|
||||||
|
function startTimer() {
|
||||||
|
startTimeRef.current = Date.now();
|
||||||
|
intervalRef.current = setInterval(() => {
|
||||||
|
const elapsed = (Date.now() - startTimeRef.current) / 1000;
|
||||||
|
const left = Math.max(0, BLITZ_DURATION - elapsed);
|
||||||
|
setTimeLeft(left);
|
||||||
|
if (left <= 0) endGame();
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function endGame() {
|
||||||
|
if (endedRef.current) return;
|
||||||
|
endedRef.current = true;
|
||||||
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||||
|
setPhase("ended");
|
||||||
|
// Sauvegarder
|
||||||
|
fetch("/api/games", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
mode: "blitz",
|
||||||
|
startArticle: visitedListRef.current[0] ?? "",
|
||||||
|
targetArticle: "",
|
||||||
|
path: visitedListRef.current,
|
||||||
|
clicks: clicksRef.current,
|
||||||
|
timeSeconds: BLITZ_DURATION,
|
||||||
|
won: true,
|
||||||
|
}),
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => () => { if (intervalRef.current) clearInterval(intervalRef.current); }, []);
|
||||||
|
|
||||||
|
async function loadArticle(t: string) {
|
||||||
|
setLoadError(null);
|
||||||
|
setLoading(true);
|
||||||
|
loadingRef.current = true;
|
||||||
|
const art = await fetchArticle(t);
|
||||||
|
setLoading(false);
|
||||||
|
loadingRef.current = false;
|
||||||
|
if (!art) { setLoadError(`Impossible de charger "${t}".`); return null; }
|
||||||
|
setHtml(art.html);
|
||||||
|
setTitle(art.title);
|
||||||
|
return art.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
setLoading(true);
|
||||||
|
clicksRef.current = 0; setClicks(0);
|
||||||
|
visitedRef.current = new Set();
|
||||||
|
visitedListRef.current = [];
|
||||||
|
endedRef.current = false;
|
||||||
|
setTimeLeft(BLITZ_DURATION);
|
||||||
|
setVisited([]);
|
||||||
|
setLoadError(null);
|
||||||
|
|
||||||
|
const { start: startArticle } = await pickTwoArticles();
|
||||||
|
const canonical = await loadArticle(startArticle);
|
||||||
|
if (!canonical) return;
|
||||||
|
|
||||||
|
visitedRef.current.add(canonical);
|
||||||
|
visitedListRef.current = [canonical];
|
||||||
|
setVisited([canonical]);
|
||||||
|
setPhase("playing");
|
||||||
|
startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigate = useCallback(async (t: string) => {
|
||||||
|
if (loadingRef.current || endedRef.current) return;
|
||||||
|
clicksRef.current += 1;
|
||||||
|
setClicks(clicksRef.current);
|
||||||
|
|
||||||
|
const canonical = await loadArticle(t);
|
||||||
|
if (!canonical) return;
|
||||||
|
|
||||||
|
if (!visitedRef.current.has(canonical)) {
|
||||||
|
visitedRef.current.add(canonical);
|
||||||
|
visitedListRef.current = [...visitedListRef.current, canonical];
|
||||||
|
setVisited([...visitedListRef.current]);
|
||||||
|
}
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
if (intervalRef.current) clearInterval(intervalRef.current);
|
||||||
|
endedRef.current = false;
|
||||||
|
clicksRef.current = 0; setClicks(0);
|
||||||
|
visitedRef.current = new Set();
|
||||||
|
visitedListRef.current = [];
|
||||||
|
setVisited([]);
|
||||||
|
setHtml(""); setTitle("");
|
||||||
|
setTimeLeft(BLITZ_DURATION);
|
||||||
|
setPhase("setup");
|
||||||
|
setLoadError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
phase, html, title, loading, loadError,
|
||||||
|
timeLeft, clicks, visited,
|
||||||
|
start, navigate, reset,
|
||||||
|
retryLoad: () => title && loadArticle(title),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useCallback, useRef, useEffect } from "react";
|
||||||
|
import { fetchArticle, normalizeTitle } from "./wiki";
|
||||||
|
import { useTimer } from "./useTimer";
|
||||||
|
|
||||||
|
export type DailyPhase = "loading" | "playing" | "won" | "gave_up" | "already_played";
|
||||||
|
|
||||||
|
export type DailyPuzzleInfo = {
|
||||||
|
id: string;
|
||||||
|
date: string;
|
||||||
|
startArticle: string;
|
||||||
|
targetArticle: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DailyResult = {
|
||||||
|
clicks: number;
|
||||||
|
timeSeconds: number;
|
||||||
|
won: boolean;
|
||||||
|
path: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useDailyGame() {
|
||||||
|
const timer = useTimer();
|
||||||
|
const [phase, setPhase] = useState<DailyPhase>("loading");
|
||||||
|
const [puzzle, setPuzzle] = useState<DailyPuzzleInfo | null>(null);
|
||||||
|
const [myResult, setMyResult] = useState<DailyResult | null>(null);
|
||||||
|
|
||||||
|
const [html, setHtml] = useState("");
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [loadError, setLoadError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const clicksRef = useRef(0);
|
||||||
|
const [clicks, setClicks] = useState(0);
|
||||||
|
const pathRef = useRef<string[]>([]);
|
||||||
|
const [history, setHistory] = useState<string[]>([]);
|
||||||
|
const timerStartedRef = useRef(false);
|
||||||
|
const gameEndedRef = useRef(false);
|
||||||
|
const loadingRef = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/daily")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then(async (data: { puzzle: DailyPuzzleInfo; alreadyPlayed: boolean; myResult: DailyResult | null }) => {
|
||||||
|
setPuzzle(data.puzzle);
|
||||||
|
if (data.alreadyPlayed && data.myResult) {
|
||||||
|
setMyResult(data.myResult);
|
||||||
|
setPhase("already_played");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Charger l'article de départ
|
||||||
|
const art = await fetchArticle(data.puzzle.startArticle);
|
||||||
|
if (!art) { setLoadError("Impossible de charger l'article de départ."); return; }
|
||||||
|
setHtml(art.html);
|
||||||
|
setTitle(art.title);
|
||||||
|
pathRef.current = [art.title];
|
||||||
|
setHistory([art.title]);
|
||||||
|
setPhase("playing");
|
||||||
|
})
|
||||||
|
.catch(() => setLoadError("Impossible de charger le défi du jour."));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function loadArticle(t: string) {
|
||||||
|
setLoadError(null);
|
||||||
|
setLoading(true);
|
||||||
|
loadingRef.current = true;
|
||||||
|
const art = await fetchArticle(t);
|
||||||
|
setLoading(false);
|
||||||
|
loadingRef.current = false;
|
||||||
|
if (!art) { setLoadError(`Impossible de charger "${t}".`); return null; }
|
||||||
|
setHtml(art.html);
|
||||||
|
setTitle(art.title);
|
||||||
|
return art.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitResult(won: boolean) {
|
||||||
|
if (!puzzle) return;
|
||||||
|
const result: DailyResult = {
|
||||||
|
clicks: clicksRef.current,
|
||||||
|
timeSeconds: timer.elapsed,
|
||||||
|
won,
|
||||||
|
path: pathRef.current,
|
||||||
|
};
|
||||||
|
setMyResult(result);
|
||||||
|
await fetch("/api/daily", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ puzzleId: puzzle.id, ...result, path: result.path }),
|
||||||
|
}).catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const navigate = useCallback(async (t: string) => {
|
||||||
|
if (loadingRef.current || gameEndedRef.current) return;
|
||||||
|
clicksRef.current += 1;
|
||||||
|
setClicks(clicksRef.current);
|
||||||
|
if (!timerStartedRef.current) { timer.start(); timerStartedRef.current = true; }
|
||||||
|
|
||||||
|
const canonical = await loadArticle(t);
|
||||||
|
if (!canonical) return;
|
||||||
|
const newPath = [...pathRef.current, canonical];
|
||||||
|
pathRef.current = newPath;
|
||||||
|
setHistory(newPath);
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
|
||||||
|
if (puzzle && normalizeTitle(canonical) === normalizeTitle(puzzle.targetArticle)) {
|
||||||
|
timer.stop();
|
||||||
|
gameEndedRef.current = true;
|
||||||
|
setPhase("won");
|
||||||
|
await submitResult(true);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [puzzle]);
|
||||||
|
|
||||||
|
const goBack = useCallback(async () => {
|
||||||
|
if (loadingRef.current || gameEndedRef.current || pathRef.current.length <= 1) return;
|
||||||
|
clicksRef.current += 1;
|
||||||
|
setClicks(clicksRef.current);
|
||||||
|
if (!timerStartedRef.current) { timer.start(); timerStartedRef.current = true; }
|
||||||
|
const newPath = pathRef.current.slice(0, -1);
|
||||||
|
const canonical = await loadArticle(newPath[newPath.length - 1]);
|
||||||
|
if (!canonical) return;
|
||||||
|
pathRef.current = newPath;
|
||||||
|
setHistory(newPath);
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function giveUp() {
|
||||||
|
timer.stop();
|
||||||
|
gameEndedRef.current = true;
|
||||||
|
setPhase("gave_up");
|
||||||
|
await submitResult(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
phase, puzzle, myResult,
|
||||||
|
html, title, loading, loadError,
|
||||||
|
history, clicks, elapsed: timer.elapsed,
|
||||||
|
canGoBack: pathRef.current.length > 1,
|
||||||
|
navigate, goBack, giveUp,
|
||||||
|
retryLoad: () => title && loadArticle(title),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "DailyPuzzle" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"date" TEXT NOT NULL,
|
||||||
|
"startArticle" TEXT NOT NULL,
|
||||||
|
"targetArticle" TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "DailyResult" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"puzzleId" TEXT NOT NULL,
|
||||||
|
"userId" TEXT NOT NULL,
|
||||||
|
"path" TEXT NOT NULL,
|
||||||
|
"clicks" INTEGER NOT NULL,
|
||||||
|
"timeSeconds" REAL NOT NULL,
|
||||||
|
"won" BOOLEAN NOT NULL,
|
||||||
|
"playedAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "DailyResult_puzzleId_fkey" FOREIGN KEY ("puzzleId") REFERENCES "DailyPuzzle" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "DailyResult_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "DailyPuzzle_date_key" ON "DailyPuzzle"("date");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "DailyResult_puzzleId_idx" ON "DailyResult"("puzzleId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "DailyResult_puzzleId_userId_key" ON "DailyResult"("puzzleId", "userId");
|
||||||
+27
-2
@@ -15,14 +15,15 @@ model User {
|
|||||||
email String @unique
|
email String @unique
|
||||||
password String
|
password String
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
games Game[]
|
games Game[]
|
||||||
|
dailyResults DailyResult[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model Game {
|
model Game {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
userId String
|
userId String
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
mode String // "solo" | "multi"
|
mode String // "solo" | "multi" | "daily" | "blitz"
|
||||||
startArticle String
|
startArticle String
|
||||||
targetArticle String
|
targetArticle String
|
||||||
path String // JSON array de titres
|
path String // JSON array de titres
|
||||||
@@ -33,3 +34,27 @@ model Game {
|
|||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model DailyPuzzle {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
date String @unique // "YYYY-MM-DD"
|
||||||
|
startArticle String
|
||||||
|
targetArticle String
|
||||||
|
results DailyResult[]
|
||||||
|
}
|
||||||
|
|
||||||
|
model DailyResult {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
puzzleId String
|
||||||
|
puzzle DailyPuzzle @relation(fields: [puzzleId], references: [id], onDelete: Cascade)
|
||||||
|
userId String
|
||||||
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
|
path String // JSON
|
||||||
|
clicks Int
|
||||||
|
timeSeconds Float
|
||||||
|
won Boolean
|
||||||
|
playedAt DateTime @default(now())
|
||||||
|
|
||||||
|
@@unique([puzzleId, userId])
|
||||||
|
@@index([puzzleId])
|
||||||
|
}
|
||||||
|
|||||||
BIN
Binary file not shown.
Reference in New Issue
Block a user