feat: add daily challenge and blitz mode (2min speedrun)

This commit is contained in:
jessy-david-dev
2026-04-10 21:41:49 +02:00
parent 8c18984121
commit dc12abf796
21 changed files with 4035 additions and 12 deletions
+23
View File
@@ -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,
})));
}
+61
View File
@@ -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 });
}
+116
View File
@@ -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&apos;articles Wikipedia différents en cliquant sur les liens. Ton score = nombre d&apos;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>
);
}
+175
View File
@@ -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&apos;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&apos;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>
);
}
+16 -2
View File
@@ -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>
+12
View File
@@ -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)} />}
</> </>
+10
View File
@@ -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
+10
View File
@@ -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'
+2
View File
@@ -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
+114
View File
@@ -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
View File
@@ -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;
+132
View File
@@ -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),
};
}
+144
View File
@@ -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
View File
@@ -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
View File
Binary file not shown.