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
+10
View File
@@ -27,3 +27,13 @@ export type User = Prisma.UserModel
*
*/
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
/**
* 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 = {
User: 'User',
Game: 'Game'
Game: 'Game',
DailyPuzzle: 'DailyPuzzle',
DailyResult: 'DailyResult'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -401,7 +403,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "game"
modelProps: "user" | "game" | "dailyPuzzle" | "dailyResult"
txIsolationLevel: TransactionIsolationLevel
}
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: {
@@ -616,6 +766,30 @@ export const 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 = {
asc: 'asc',
desc: 'desc'
@@ -761,6 +935,8 @@ export type PrismaClientOptions = ({
export type GlobalOmitConfig = {
user?: Prisma.UserOmit
game?: Prisma.GameOmit
dailyPuzzle?: Prisma.DailyPuzzleOmit
dailyResult?: Prisma.DailyResultOmit
}
/* Types for Logging */
@@ -52,7 +52,9 @@ export const AnyNull = runtime.AnyNull
export const ModelName = {
User: 'User',
Game: 'Game'
Game: 'Game',
DailyPuzzle: 'DailyPuzzle',
DailyResult: 'DailyResult'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -95,6 +97,30 @@ export const 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 = {
asc: 'asc',
desc: 'desc'
+2
View File
@@ -10,4 +10,6 @@
*/
export type * from './models/User'
export type * from './models/Game'
export type * from './models/DailyPuzzle'
export type * from './models/DailyResult'
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
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
games?: Prisma.GameListRelationFilter
dailyResults?: Prisma.DailyResultListRelationFilter
}
export type UserOrderByWithRelationInput = {
@@ -192,6 +193,7 @@ export type UserOrderByWithRelationInput = {
password?: Prisma.SortOrder
createdAt?: Prisma.SortOrder
games?: Prisma.GameOrderByRelationAggregateInput
dailyResults?: Prisma.DailyResultOrderByRelationAggregateInput
}
export type UserWhereUniqueInput = Prisma.AtLeast<{
@@ -204,6 +206,7 @@ export type UserWhereUniqueInput = Prisma.AtLeast<{
password?: Prisma.StringFilter<"User"> | string
createdAt?: Prisma.DateTimeFilter<"User"> | Date | string
games?: Prisma.GameListRelationFilter
dailyResults?: Prisma.DailyResultListRelationFilter
}, "id" | "email">
export type UserOrderByWithAggregationInput = {
@@ -235,6 +238,7 @@ export type UserCreateInput = {
password: string
createdAt?: Date | string
games?: Prisma.GameCreateNestedManyWithoutUserInput
dailyResults?: Prisma.DailyResultCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateInput = {
@@ -244,6 +248,7 @@ export type UserUncheckedCreateInput = {
password: string
createdAt?: Date | string
games?: Prisma.GameUncheckedCreateNestedManyWithoutUserInput
dailyResults?: Prisma.DailyResultUncheckedCreateNestedManyWithoutUserInput
}
export type UserUpdateInput = {
@@ -253,6 +258,7 @@ export type UserUpdateInput = {
password?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
games?: Prisma.GameUpdateManyWithoutUserNestedInput
dailyResults?: Prisma.DailyResultUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateInput = {
@@ -262,6 +268,7 @@ export type UserUncheckedUpdateInput = {
password?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
games?: Prisma.GameUncheckedUpdateManyWithoutUserNestedInput
dailyResults?: Prisma.DailyResultUncheckedUpdateManyWithoutUserNestedInput
}
export type UserCreateManyInput = {
@@ -339,12 +346,27 @@ export type UserUpdateOneRequiredWithoutGamesNestedInput = {
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 = {
id?: string
name: string
email: string
password: string
createdAt?: Date | string
dailyResults?: Prisma.DailyResultCreateNestedManyWithoutUserInput
}
export type UserUncheckedCreateWithoutGamesInput = {
@@ -353,6 +375,7 @@ export type UserUncheckedCreateWithoutGamesInput = {
email: string
password: string
createdAt?: Date | string
dailyResults?: Prisma.DailyResultUncheckedCreateNestedManyWithoutUserInput
}
export type UserCreateOrConnectWithoutGamesInput = {
@@ -377,6 +400,7 @@ export type UserUpdateWithoutGamesInput = {
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | string
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
dailyResults?: Prisma.DailyResultUpdateManyWithoutUserNestedInput
}
export type UserUncheckedUpdateWithoutGamesInput = {
@@ -385,6 +409,59 @@ export type UserUncheckedUpdateWithoutGamesInput = {
email?: Prisma.StringFieldUpdateOperationsInput | string
password?: Prisma.StringFieldUpdateOperationsInput | 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 = {
games: number
dailyResults: number
}
export type UserCountOutputTypeSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
games?: boolean | UserCountOutputTypeCountGamesArgs
dailyResults?: boolean | UserCountOutputTypeCountDailyResultsArgs
}
/**
@@ -417,6 +496,13 @@ export type UserCountOutputTypeCountGamesArgs<ExtArgs extends runtime.Types.Exte
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<{
id?: boolean
@@ -425,6 +511,7 @@ export type UserSelect<ExtArgs extends runtime.Types.Extensions.InternalArgs = r
password?: boolean
createdAt?: boolean
games?: boolean | Prisma.User$gamesArgs<ExtArgs>
dailyResults?: boolean | Prisma.User$dailyResultsArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}, 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 UserInclude<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
games?: boolean | Prisma.User$gamesArgs<ExtArgs>
dailyResults?: boolean | Prisma.User$dailyResultsArgs<ExtArgs>
_count?: boolean | Prisma.UserCountOutputTypeDefaultArgs<ExtArgs>
}
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"
objects: {
games: Prisma.$GamePayload<ExtArgs>[]
dailyResults: Prisma.$DailyResultPayload<ExtArgs>[]
}
scalars: runtime.Types.Extensions.GetPayloadResult<{
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> {
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>
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.
* @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[]
}
/**
* 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
*/
+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 = {
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),
};
}