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

- Implement user registration and login with NextAuth v5 (email/password, JWT)
- Add authentication modal in UI with login/register tabs
- Create user profile screen showing game statistics and history
- Integrate Prisma 7 ORM with SQLite database for data persistence
- Store game results (mode, path, clicks, time) in database
- Auto-save completed games only when user is authenticated
- Separate business logic into reusable hooks (useSoloGame, useMultiGame)
- Organize UI into composable screen components (HomeScreen, SoloScreen, ProfileScreen, etc)
- Add session persistence across F5 refresh for solo and multiplayer
- Style auth modal, account button, and profile stats dashboard
This commit is contained in:
jessy-david-dev
2026-04-10 15:43:07 +02:00
parent 6a75e80e6c
commit dc09658fdb
44 changed files with 12371 additions and 91 deletions
+29
View File
@@ -0,0 +1,29 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma-related types and utilities in a browser.
* Use it to get access to models, enums, and input types.
*
* This file does not contain a `PrismaClient` class, nor several other helpers that are intended as server-side only.
* See `client.ts` for the standard, server-side entry point.
*
* 🟢 You can import this file directly.
*/
import * as Prisma from './internal/prismaNamespaceBrowser'
export { Prisma }
export * as $Enums from './enums'
export * from './enums';
/**
* Model User
*
*/
export type User = Prisma.UserModel
/**
* Model Game
*
*/
export type Game = Prisma.GameModel
+53
View File
@@ -0,0 +1,53 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
* If you're looking for something you can import in the client-side of your application, please refer to the `browser.ts` file instead.
*
* 🟢 You can import this file directly.
*/
import * as process from 'node:process'
import * as path from 'node:path'
import { fileURLToPath } from 'node:url'
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
import * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums"
import * as $Class from "./internal/class"
import * as Prisma from "./internal/prismaNamespace"
export * as $Enums from './enums'
export * from "./enums"
/**
* ## Prisma Client
*
* Type-safe database client for TypeScript
* @example
* ```
* const prisma = new PrismaClient({
* adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL })
* })
* // Fetch zero or more Users
* const users = await prisma.user.findMany()
* ```
*
* Read more in our [docs](https://pris.ly/d/client).
*/
export const PrismaClient = $Class.getPrismaClientClass()
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
export { Prisma }
/**
* Model User
*
*/
export type User = Prisma.UserModel
/**
* Model Game
*
*/
export type Game = Prisma.GameModel
+263
View File
@@ -0,0 +1,263 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports various common sort, input & filter types that are not directly linked to a particular model.
*
* 🟢 You can import this file directly.
*/
import type * as runtime from "@prisma/client/runtime/client"
import * as $Enums from "./enums"
import type * as Prisma from "./internal/prismaNamespace"
export type StringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type DateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[]
notIn?: Date[] | string[]
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type StringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type DateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[]
notIn?: Date[] | string[]
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type IntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type FloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type BoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type IntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type FloatWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedFloatFilter<$PrismaModel>
_min?: Prisma.NestedFloatFilter<$PrismaModel>
_max?: Prisma.NestedFloatFilter<$PrismaModel>
}
export type BoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringFilter<$PrismaModel> | string
}
export type NestedDateTimeFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[]
notIn?: Date[] | string[]
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeFilter<$PrismaModel> | Date | string
}
export type NestedStringWithAggregatesFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[]
notIn?: string[]
lt?: string | Prisma.StringFieldRefInput<$PrismaModel>
lte?: string | Prisma.StringFieldRefInput<$PrismaModel>
gt?: string | Prisma.StringFieldRefInput<$PrismaModel>
gte?: string | Prisma.StringFieldRefInput<$PrismaModel>
contains?: string | Prisma.StringFieldRefInput<$PrismaModel>
startsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
endsWith?: string | Prisma.StringFieldRefInput<$PrismaModel>
not?: Prisma.NestedStringWithAggregatesFilter<$PrismaModel> | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedStringFilter<$PrismaModel>
_max?: Prisma.NestedStringFilter<$PrismaModel>
}
export type NestedIntFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntFilter<$PrismaModel> | number
}
export type NestedDateTimeWithAggregatesFilter<$PrismaModel = never> = {
equals?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
in?: Date[] | string[]
notIn?: Date[] | string[]
lt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
lte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gt?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
gte?: Date | string | Prisma.DateTimeFieldRefInput<$PrismaModel>
not?: Prisma.NestedDateTimeWithAggregatesFilter<$PrismaModel> | Date | string
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedDateTimeFilter<$PrismaModel>
_max?: Prisma.NestedDateTimeFilter<$PrismaModel>
}
export type NestedFloatFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatFilter<$PrismaModel> | number
}
export type NestedBoolFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolFilter<$PrismaModel> | boolean
}
export type NestedIntWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.IntFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
lt?: number | Prisma.IntFieldRefInput<$PrismaModel>
lte?: number | Prisma.IntFieldRefInput<$PrismaModel>
gt?: number | Prisma.IntFieldRefInput<$PrismaModel>
gte?: number | Prisma.IntFieldRefInput<$PrismaModel>
not?: Prisma.NestedIntWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedIntFilter<$PrismaModel>
_max?: Prisma.NestedIntFilter<$PrismaModel>
}
export type NestedFloatWithAggregatesFilter<$PrismaModel = never> = {
equals?: number | Prisma.FloatFieldRefInput<$PrismaModel>
in?: number[]
notIn?: number[]
lt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
lte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gt?: number | Prisma.FloatFieldRefInput<$PrismaModel>
gte?: number | Prisma.FloatFieldRefInput<$PrismaModel>
not?: Prisma.NestedFloatWithAggregatesFilter<$PrismaModel> | number
_count?: Prisma.NestedIntFilter<$PrismaModel>
_avg?: Prisma.NestedFloatFilter<$PrismaModel>
_sum?: Prisma.NestedFloatFilter<$PrismaModel>
_min?: Prisma.NestedFloatFilter<$PrismaModel>
_max?: Prisma.NestedFloatFilter<$PrismaModel>
}
export type NestedBoolWithAggregatesFilter<$PrismaModel = never> = {
equals?: boolean | Prisma.BooleanFieldRefInput<$PrismaModel>
not?: Prisma.NestedBoolWithAggregatesFilter<$PrismaModel> | boolean
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedBoolFilter<$PrismaModel>
_max?: Prisma.NestedBoolFilter<$PrismaModel>
}
+15
View File
@@ -0,0 +1,15 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This file exports all enum related types from the schema.
*
* 🟢 You can import this file directly.
*/
// This file is empty because there are no enums in the schema.
export {}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,826 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the client.ts file.
* While this enables partial backward compatibility, it is not part of the stable public API.
*
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
* model files in the `model` directory!
*/
import * as runtime from "@prisma/client/runtime/client"
import type * as Prisma from "../models"
import { type PrismaClient } from "./class"
export type * from '../models'
export type DMMF = typeof runtime.DMMF
export type PrismaPromise<T> = runtime.Types.Public.PrismaPromise<T>
/**
* Prisma Errors
*/
export const PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
export type PrismaClientKnownRequestError = runtime.PrismaClientKnownRequestError
export const PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
export type PrismaClientUnknownRequestError = runtime.PrismaClientUnknownRequestError
export const PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
export type PrismaClientRustPanicError = runtime.PrismaClientRustPanicError
export const PrismaClientInitializationError = runtime.PrismaClientInitializationError
export type PrismaClientInitializationError = runtime.PrismaClientInitializationError
export const PrismaClientValidationError = runtime.PrismaClientValidationError
export type PrismaClientValidationError = runtime.PrismaClientValidationError
/**
* Re-export of sql-template-tag
*/
export const sql = runtime.sqltag
export const empty = runtime.empty
export const join = runtime.join
export const raw = runtime.raw
export const Sql = runtime.Sql
export type Sql = runtime.Sql
/**
* Decimal.js
*/
export const Decimal = runtime.Decimal
export type Decimal = runtime.Decimal
export type DecimalJsLike = runtime.DecimalJsLike
/**
* Extensions
*/
export type Extension = runtime.Types.Extensions.UserArgs
export const getExtensionContext = runtime.Extensions.getExtensionContext
export type Args<T, F extends runtime.Operation> = runtime.Types.Public.Args<T, F>
export type Payload<T, F extends runtime.Operation = never> = runtime.Types.Public.Payload<T, F>
export type Result<T, A, F extends runtime.Operation> = runtime.Types.Public.Result<T, A, F>
export type Exact<A, W> = runtime.Types.Public.Exact<A, W>
export type PrismaVersion = {
client: string
engine: string
}
/**
* Prisma Client JS version: 7.7.0
* Query Engine version: 75cbdc1eb7150937890ad5465d861175c6624711
*/
export const prismaVersion: PrismaVersion = {
client: "7.7.0",
engine: "75cbdc1eb7150937890ad5465d861175c6624711"
}
/**
* Utility Types
*/
export type Bytes = runtime.Bytes
export type JsonObject = runtime.JsonObject
export type JsonArray = runtime.JsonArray
export type JsonValue = runtime.JsonValue
export type InputJsonObject = runtime.InputJsonObject
export type InputJsonArray = runtime.InputJsonArray
export type InputJsonValue = runtime.InputJsonValue
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
}
/**
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const DbNull = runtime.DbNull
/**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const JsonNull = runtime.JsonNull
/**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const AnyNull = runtime.AnyNull
type SelectAndInclude = {
select: any
include: any
}
type SelectAndOmit = {
select: any
omit: any
}
/**
* From T, pick a set of properties whose keys are in the union K
*/
type Prisma__Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
export type Enumerable<T> = T | Array<T>;
/**
* Subset
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection
*/
export type Subset<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : never;
};
/**
* SelectSubset
* @desc From `T` pick properties that exist in `U`. Simple version of Intersection.
* Additionally, it validates, if both select and include are present. If the case, it errors.
*/
export type SelectSubset<T, U> = {
[key in keyof T]: key extends keyof U ? T[key] : never
} &
(T extends SelectAndInclude
? 'Please either choose `select` or `include`.'
: T extends SelectAndOmit
? 'Please either choose `select` or `omit`.'
: {})
/**
* Subset + Intersection
* @desc From `T` pick properties that exist in `U` and intersect `K`
*/
export type SubsetIntersection<T, U, K> = {
[key in keyof T]: key extends keyof U ? T[key] : never
} &
K
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
/**
* XOR is needed to have a real mutually exclusive union type
* https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types
*/
export type XOR<T, U> =
T extends object ?
U extends object ?
(Without<T, U> & U) | (Without<U, T> & T)
: U : T
/**
* Is T a Record?
*/
type IsObject<T extends any> = T extends Array<any>
? False
: T extends Date
? False
: T extends Uint8Array
? False
: T extends BigInt
? False
: T extends object
? True
: False
/**
* If it's T[], return T
*/
export type UnEnumerate<T extends unknown> = T extends Array<infer U> ? U : T
/**
* From ts-toolbelt
*/
type __Either<O extends object, K extends Key> = Omit<O, K> &
{
// Merge all but K
[P in K]: Prisma__Pick<O, P & keyof O> // With K possibilities
}[K]
type EitherStrict<O extends object, K extends Key> = Strict<__Either<O, K>>
type EitherLoose<O extends object, K extends Key> = ComputeRaw<__Either<O, K>>
type _Either<
O extends object,
K extends Key,
strict extends Boolean
> = {
1: EitherStrict<O, K>
0: EitherLoose<O, K>
}[strict]
export type Either<
O extends object,
K extends Key,
strict extends Boolean = 1
> = O extends unknown ? _Either<O, K, strict> : never
export type Union = any
export type PatchUndefined<O extends object, O1 extends object> = {
[K in keyof O]: O[K] extends undefined ? At<O1, K> : O[K]
} & {}
/** Helper Types for "Merge" **/
export type IntersectOf<U extends Union> = (
U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never
export type Overwrite<O extends object, O1 extends object> = {
[K in keyof O]: K extends keyof O1 ? O1[K] : O[K];
} & {};
type _Merge<U extends object> = IntersectOf<Overwrite<U, {
[K in keyof U]-?: At<U, K>;
}>>;
type Key = string | number | symbol;
type AtStrict<O extends object, K extends Key> = O[K & keyof O];
type AtLoose<O extends object, K extends Key> = O extends unknown ? AtStrict<O, K> : never;
export type At<O extends object, K extends Key, strict extends Boolean = 1> = {
1: AtStrict<O, K>;
0: AtLoose<O, K>;
}[strict];
export type ComputeRaw<A extends any> = A extends Function ? A : {
[K in keyof A]: A[K];
} & {};
export type OptionalFlat<O> = {
[K in keyof O]?: O[K];
} & {};
type _Record<K extends keyof any, T> = {
[P in K]: T;
};
// cause typescript not to expand types and preserve names
type NoExpand<T> = T extends unknown ? T : never;
// this type assumes the passed object is entirely optional
export type AtLeast<O extends object, K extends string> = NoExpand<
O extends unknown
? | (K extends keyof O ? { [P in K]: O[P] } & O : O)
| {[P in keyof O as P extends K ? P : never]-?: O[P]} & O
: never>;
type _Strict<U, _U = U> = U extends unknown ? U & OptionalFlat<_Record<Exclude<Keys<_U>, keyof U>, never>> : never;
export type Strict<U extends object> = ComputeRaw<_Strict<U>>;
/** End Helper Types for "Merge" **/
export type Merge<U extends object> = ComputeRaw<_Merge<Strict<U>>>;
export type Boolean = True | False
export type True = 1
export type False = 0
export type Not<B extends Boolean> = {
0: 1
1: 0
}[B]
export type Extends<A1 extends any, A2 extends any> = [A1] extends [never]
? 0 // anything `never` is false
: A1 extends A2
? 1
: 0
export type Has<U extends Union, U1 extends Union> = Not<
Extends<Exclude<U1, U>, U1>
>
export type Or<B1 extends Boolean, B2 extends Boolean> = {
0: {
0: 0
1: 1
}
1: {
0: 1
1: 1
}
}[B1][B2]
export type Keys<U extends Union> = U extends unknown ? keyof U : never
export type GetScalarType<T, O> = O extends object ? {
[P in keyof T]: P extends keyof O
? O[P]
: never
} : never
type FieldPaths<
T,
U = Omit<T, '_avg' | '_sum' | '_count' | '_min' | '_max'>
> = IsObject<T> extends True ? U : T
export type GetHavingFields<T> = {
[K in keyof T]: Or<
Or<Extends<'OR', K>, Extends<'AND', K>>,
Extends<'NOT', K>
> extends True
? // infer is only needed to not hit TS limit
// based on the brilliant idea of Pierre-Antoine Mills
// https://github.com/microsoft/TypeScript/issues/30188#issuecomment-478938437
T[K] extends infer TK
? GetHavingFields<UnEnumerate<TK> extends object ? Merge<UnEnumerate<TK>> : never>
: never
: {} extends FieldPaths<T[K]>
? never
: K
}[keyof T]
/**
* Convert tuple to union
*/
type _TupleToUnion<T> = T extends (infer E)[] ? E : never
type TupleToUnion<K extends readonly any[]> = _TupleToUnion<K>
export type MaybeTupleToUnion<T> = T extends any[] ? TupleToUnion<T> : T
/**
* Like `Pick`, but additionally can also accept an array of keys
*/
export type PickEnumerable<T, K extends Enumerable<keyof T> | keyof T> = Prisma__Pick<T, MaybeTupleToUnion<K>>
/**
* Exclude all keys with underscores
*/
export type ExcludeUnderscoreKeys<T extends string> = T extends `_${string}` ? never : T
export type FieldRef<Model, FieldType> = runtime.FieldRef<Model, FieldType>
type FieldRefInputType<Model, FieldType> = Model extends never ? never : FieldRef<Model, FieldType>
export const ModelName = {
User: 'User',
Game: 'Game'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
export interface TypeMapCb<GlobalOmitOptions = {}> extends runtime.Types.Utils.Fn<{extArgs: runtime.Types.Extensions.InternalArgs }, runtime.Types.Utils.Record<string, any>> {
returns: TypeMap<this['params']['extArgs'], GlobalOmitOptions>
}
export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs, GlobalOmitOptions = {}> = {
globalOmitOptions: {
omit: GlobalOmitOptions
}
meta: {
modelProps: "user" | "game"
txIsolationLevel: TransactionIsolationLevel
}
model: {
User: {
payload: Prisma.$UserPayload<ExtArgs>
fields: Prisma.UserFieldRefs
operations: {
findUnique: {
args: Prisma.UserFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload> | null
}
findUniqueOrThrow: {
args: Prisma.UserFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
findFirst: {
args: Prisma.UserFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload> | null
}
findFirstOrThrow: {
args: Prisma.UserFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
findMany: {
args: Prisma.UserFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>[]
}
create: {
args: Prisma.UserCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
createMany: {
args: Prisma.UserCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.UserCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>[]
}
delete: {
args: Prisma.UserDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
update: {
args: Prisma.UserUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
deleteMany: {
args: Prisma.UserDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.UserUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.UserUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>[]
}
upsert: {
args: Prisma.UserUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$UserPayload>
}
aggregate: {
args: Prisma.UserAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateUser>
}
groupBy: {
args: Prisma.UserGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.UserGroupByOutputType>[]
}
count: {
args: Prisma.UserCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.UserCountAggregateOutputType> | number
}
}
}
Game: {
payload: Prisma.$GamePayload<ExtArgs>
fields: Prisma.GameFieldRefs
operations: {
findUnique: {
args: Prisma.GameFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$GamePayload> | null
}
findUniqueOrThrow: {
args: Prisma.GameFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$GamePayload>
}
findFirst: {
args: Prisma.GameFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$GamePayload> | null
}
findFirstOrThrow: {
args: Prisma.GameFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$GamePayload>
}
findMany: {
args: Prisma.GameFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$GamePayload>[]
}
create: {
args: Prisma.GameCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$GamePayload>
}
createMany: {
args: Prisma.GameCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.GameCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$GamePayload>[]
}
delete: {
args: Prisma.GameDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$GamePayload>
}
update: {
args: Prisma.GameUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$GamePayload>
}
deleteMany: {
args: Prisma.GameDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.GameUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.GameUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$GamePayload>[]
}
upsert: {
args: Prisma.GameUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$GamePayload>
}
aggregate: {
args: Prisma.GameAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateGame>
}
groupBy: {
args: Prisma.GameGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.GameGroupByOutputType>[]
}
count: {
args: Prisma.GameCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.GameCountAggregateOutputType> | number
}
}
}
}
} & {
other: {
payload: any
operations: {
$executeRaw: {
args: [query: TemplateStringsArray | Sql, ...values: any[]],
result: any
}
$executeRawUnsafe: {
args: [query: string, ...values: any[]],
result: any
}
$queryRaw: {
args: [query: TemplateStringsArray | Sql, ...values: any[]],
result: any
}
$queryRawUnsafe: {
args: [query: string, ...values: any[]],
result: any
}
}
}
}
/**
* Enums
*/
export const TransactionIsolationLevel = runtime.makeStrictEnum({
Serializable: 'Serializable'
} as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const UserScalarFieldEnum = {
id: 'id',
name: 'name',
email: 'email',
password: 'password',
createdAt: 'createdAt'
} as const
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
export const GameScalarFieldEnum = {
id: 'id',
userId: 'userId',
mode: 'mode',
startArticle: 'startArticle',
targetArticle: 'targetArticle',
path: 'path',
clicks: 'clicks',
timeSeconds: 'timeSeconds',
won: 'won',
playedAt: 'playedAt'
} as const
export type GameScalarFieldEnum = (typeof GameScalarFieldEnum)[keyof typeof GameScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
} as const
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
/**
* Field references
*/
/**
* Reference to a field of type 'String'
*/
export type StringFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'String'>
/**
* Reference to a field of type 'DateTime'
*/
export type DateTimeFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'DateTime'>
/**
* Reference to a field of type 'Int'
*/
export type IntFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Int'>
/**
* Reference to a field of type 'Float'
*/
export type FloatFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Float'>
/**
* Reference to a field of type 'Boolean'
*/
export type BooleanFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'Boolean'>
/**
* Batch Payload for updateMany & deleteMany & createMany
*/
export type BatchPayload = {
count: number
}
export const defineExtension = runtime.Extensions.defineExtension as unknown as runtime.Types.Extensions.ExtendsHook<"define", TypeMapCb, runtime.Types.Extensions.DefaultArgs>
export type DefaultPrismaClient = PrismaClient
export type ErrorFormat = 'pretty' | 'colorless' | 'minimal'
export type PrismaClientOptions = ({
/**
* Instance of a Driver Adapter, e.g., like one provided by `@prisma/adapter-pg`.
*/
adapter: runtime.SqlDriverAdapterFactory
accelerateUrl?: never
} | {
/**
* Prisma Accelerate URL allowing the client to connect through Accelerate instead of a direct database.
*/
accelerateUrl: string
adapter?: never
}) & {
/**
* @default "colorless"
*/
errorFormat?: ErrorFormat
/**
* @example
* ```
* // Shorthand for `emit: 'stdout'`
* log: ['query', 'info', 'warn', 'error']
*
* // Emit as events only
* log: [
* { emit: 'event', level: 'query' },
* { emit: 'event', level: 'info' },
* { emit: 'event', level: 'warn' }
* { emit: 'event', level: 'error' }
* ]
*
* / Emit as events and log to stdout
* og: [
* { emit: 'stdout', level: 'query' },
* { emit: 'stdout', level: 'info' },
* { emit: 'stdout', level: 'warn' }
* { emit: 'stdout', level: 'error' }
*
* ```
* Read more in our [docs](https://pris.ly/d/logging).
*/
log?: (LogLevel | LogDefinition)[]
/**
* The default values for transactionOptions
* maxWait ?= 2000
* timeout ?= 5000
*/
transactionOptions?: {
maxWait?: number
timeout?: number
isolationLevel?: TransactionIsolationLevel
}
/**
* Global configuration for omitting model fields by default.
*
* @example
* ```
* const prisma = new PrismaClient({
* omit: {
* user: {
* password: true
* }
* }
* })
* ```
*/
omit?: GlobalOmitConfig
/**
* SQL commenter plugins that add metadata to SQL queries as comments.
* Comments follow the sqlcommenter format: https://google.github.io/sqlcommenter/
*
* @example
* ```
* const prisma = new PrismaClient({
* adapter,
* comments: [
* traceContext(),
* queryInsights(),
* ],
* })
* ```
*/
comments?: runtime.SqlCommenterPlugin[]
}
export type GlobalOmitConfig = {
user?: Prisma.UserOmit
game?: Prisma.GameOmit
}
/* Types for Logging */
export type LogLevel = 'info' | 'query' | 'warn' | 'error'
export type LogDefinition = {
level: LogLevel
emit: 'stdout' | 'event'
}
export type CheckIsLogLevel<T> = T extends LogLevel ? T : never;
export type GetLogType<T> = CheckIsLogLevel<
T extends LogDefinition ? T['level'] : T
>;
export type GetEvents<T extends any[]> = T extends Array<LogLevel | LogDefinition>
? GetLogType<T[number]>
: never;
export type QueryEvent = {
timestamp: Date
query: string
params: string
duration: number
target: string
}
export type LogEvent = {
timestamp: Date
message: string
target: string
}
/* End Types for Logging */
export type PrismaAction =
| 'findUnique'
| 'findUniqueOrThrow'
| 'findMany'
| 'findFirst'
| 'findFirstOrThrow'
| 'create'
| 'createMany'
| 'createManyAndReturn'
| 'update'
| 'updateMany'
| 'updateManyAndReturn'
| 'upsert'
| 'delete'
| 'deleteMany'
| 'executeRaw'
| 'queryRaw'
| 'aggregate'
| 'count'
| 'runCommandRaw'
| 'findRaw'
| 'groupBy'
/**
* `PrismaClient` proxy available in interactive transactions.
*/
export type TransactionClient = Omit<DefaultPrismaClient, runtime.ITXClientDenyList>
@@ -0,0 +1,104 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* WARNING: This is an internal file that is subject to change!
*
* 🛑 Under no circumstances should you import this file directly! 🛑
*
* All exports from this file are wrapped under a `Prisma` namespace object in the browser.ts file.
* While this enables partial backward compatibility, it is not part of the stable public API.
*
* If you are looking for your Models, Enums, and Input Types, please import them from the respective
* model files in the `model` directory!
*/
import * as runtime from "@prisma/client/runtime/index-browser"
export type * from '../models'
export type * from './prismaNamespace'
export const Decimal = runtime.Decimal
export const NullTypes = {
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
}
/**
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const DbNull = runtime.DbNull
/**
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const JsonNull = runtime.JsonNull
/**
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
*
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
*/
export const AnyNull = runtime.AnyNull
export const ModelName = {
User: 'User',
Game: 'Game'
} as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
/*
* Enums
*/
export const TransactionIsolationLevel = runtime.makeStrictEnum({
Serializable: 'Serializable'
} as const)
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
export const UserScalarFieldEnum = {
id: 'id',
name: 'name',
email: 'email',
password: 'password',
createdAt: 'createdAt'
} as const
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
export const GameScalarFieldEnum = {
id: 'id',
userId: 'userId',
mode: 'mode',
startArticle: 'startArticle',
targetArticle: 'targetArticle',
path: 'path',
clicks: 'clicks',
timeSeconds: 'timeSeconds',
won: 'won',
playedAt: 'playedAt'
} as const
export type GameScalarFieldEnum = (typeof GameScalarFieldEnum)[keyof typeof GameScalarFieldEnum]
export const SortOrder = {
asc: 'asc',
desc: 'desc'
} as const
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
+13
View File
@@ -0,0 +1,13 @@
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
/* eslint-disable */
// biome-ignore-all lint: generated file
// @ts-nocheck
/*
* This is a barrel export file for all models and their related types.
*
* 🟢 You can import this file directly.
*/
export type * from './models/User'
export type * from './models/Game'
export type * from './commonInputTypes'
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+16
View File
@@ -0,0 +1,16 @@
import Database from "better-sqlite3";
import { PrismaBetterSqlite3 } from "@prisma/adapter-better-sqlite3";
import { PrismaClient } from "./generated/prisma/client";
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient };
function createPrisma() {
const adapter = new PrismaBetterSqlite3({ url: "./wikirace.db" });
return new PrismaClient({ adapter });
}
export const prisma = globalForPrisma.prisma ?? createPrisma();
if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma;
}
+68
View File
@@ -0,0 +1,68 @@
import type { Puzzle } from "./types";
export const FALLBACK_PUZZLES: Puzzle[] = [
{ start: "Pizza", target: "Egypte antique" },
{ start: "Michael Jackson", target: "Mont Everest" },
{ start: "Echecs", target: "Amazonie" },
{ start: "Titanic (film)", target: "Trou noir" },
{ start: "Football", target: "William Shakespeare" },
{ start: "Harry Potter", target: "Grande Muraille de Chine" },
{ start: "Albert Einstein", target: "Jazz" },
{ start: "Tour Eiffel", target: "Genetique" },
{ start: "Leonard de Vinci", target: "Eruption volcanique" },
{ start: "The Beatles", target: "Bouddhisme" },
{ start: "Dinosaure", target: "Internet" },
{ start: "Napoleon Ier", target: "Musique de jazz" },
{ start: "Cleopatre", target: "Exploration spatiale" },
{ start: "Wolfgang Amadeus Mozart", target: "Foret tropicale" },
{ start: "Isaac Newton", target: "Arts martiaux" },
{ start: "Charles Darwin", target: "Jeux olympiques" },
{ start: "Marie Curie", target: "Hip-hop" },
{ start: "Abraham Lincoln", target: "Recif corallien" },
{ start: "Ludwig van Beethoven", target: "Photographie" },
{ start: "Vincent van Gogh", target: "Tectonique des plaques" },
{ start: "Galilee", target: "Folklore" },
{ start: "Nikola Tesla", target: "Yoga" },
{ start: "Aristote", target: "Television" },
{ start: "Platon", target: "Cinema" },
{ start: "Karl Marx", target: "Surf" },
{ start: "Sigmund Freud", target: "Architecture" },
{ start: "Mahatma Gandhi", target: "Antarctique" },
{ start: "Nelson Mandela", target: "Jazz" },
{ start: "Che Guevara", target: "Sushi" },
{ start: "Barack Obama", target: "Musique classique" },
{ start: "Steve Jobs", target: "Foret amazonienne" },
{ start: "Elon Musk", target: "Dinosaure" },
{ start: "Beyonce", target: "Empire romain" },
{ start: "Taylor Swift", target: "Vikings" },
{ start: "Eminem", target: "Route de la soie" },
{ start: "Bob Dylan", target: "Samurai" },
{ start: "Freddie Mercury", target: "Fleuve Amazone" },
{ start: "David Bowie", target: "Bouddhisme" },
{ start: "Elvis Presley", target: "Mont Fuji" },
{ start: "John Lennon", target: "Trou noir" },
{ start: "Led Zeppelin", target: "Ocean" },
{ start: "Pink Floyd", target: "Democratie" },
{ start: "Nirvana (groupe)", target: "Azteques" },
{ start: "Michel-Ange", target: "Recif corallien" },
{ start: "Raphael (peintre)", target: "Rome antique" },
{ start: "Pablo Picasso", target: "Mythologie nordique" },
{ start: "Frida Kahlo", target: "Age viking" },
{ start: "Salvador Dali", target: "Mecanique quantique" },
{ start: "Andy Warhol", target: "Grande Barriere de corail" },
{ start: "Bruce Lee", target: "Mythologie grecque" },
{ start: "Muhammad Ali", target: "Route de la soie" },
{ start: "Usain Bolt", target: "Revolution francaise" },
{ start: "Serena Williams", target: "Chine antique" },
{ start: "France", target: "Japon" },
{ start: "Paris", target: "Astronomie" },
{ start: "Renaissance", target: "Biologie" },
{ start: "Philosophie", target: "Geographie" },
{ start: "Mathematiques", target: "Musique" },
{ start: "Physique", target: "Litterature" },
{ start: "Chimie", target: "Histoire" },
];
export function getFallbackPuzzle(): Puzzle {
return FALLBACK_PUZZLES[Math.floor(Math.random() * FALLBACK_PUZZLES.length)];
}
+34
View File
@@ -0,0 +1,34 @@
// Persistence sessionStorage pour F5 / rechargement de page
const KEY = "wikirace_session";
export type SessionData = {
screen: "solo" | "lobby" | "game";
// Solo
soloPuzzle?: { start: string; target: string };
soloHistory?: string[];
soloClicks?: number;
// Multi
multiRoomCode?: string;
multiPlayerId?: string;
playerName?: string;
};
export function saveSession(data: SessionData) {
try {
sessionStorage.setItem(KEY, JSON.stringify(data));
} catch { /* ignore quota */ }
}
export function loadSession(): SessionData | null {
try {
const raw = sessionStorage.getItem(KEY);
return raw ? (JSON.parse(raw) as SessionData) : null;
} catch {
return null;
}
}
export function clearSession() {
try { sessionStorage.removeItem(KEY); } catch { /* ignore */ }
}
+11
View File
@@ -0,0 +1,11 @@
export type Screen = "home" | "lobby" | "game" | "solo" | "profile";
export type WikiArticle = {
title: string;
html: string;
};
export type Puzzle = {
start: string;
target: string;
};
+253
View File
@@ -0,0 +1,253 @@
"use client";
import { useState, useCallback, useEffect, useRef } from "react";
import { fetchArticle, pickTwoArticles, prefetchArticle, POLL_INTERVAL, COUNTDOWN_DURATION } from "./wiki";
import { useTimer } from "./useTimer";
import { saveSession, clearSession } from "./session";
import type { Room } from "../app/api/rooms/route";
export function useMultiGame() {
const timer = useTimer();
const [room, setRoom] = useState<Room | null>(null);
const [playerId, setPlayerId] = useState<string | null>(null);
const clicksRef = useRef(0);
const [clicksDisplay, setClicksDisplay] = useState(0);
const [history, setHistory] = useState<string[]>([]);
const historyRef = useRef<string[]>([]);
const [html, setHtml] = useState("");
const [title, setTitle] = useState("");
const [loading, setLoading] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const loadingRef = useRef(false);
const timerStartedRef = useRef(false);
const [countdown, setCountdown] = useState<number | null>(null);
const countdownRef = useRef<ReturnType<typeof setInterval> | null>(null);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const prevPhaseRef = useRef<string | null>(null);
const prevRoundRef = useRef(0);
// Article loading
async function loadArticle(t: string): Promise<string | null> {
setLoading(true); loadingRef.current = true; setLoadError(null);
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;
}
// Countdown
function startCountdown(start: number) {
if (countdownRef.current) clearInterval(countdownRef.current);
const tick = () => {
const rem = Math.ceil((COUNTDOWN_DURATION - (Date.now() - start)) / 1000);
setCountdown(rem <= 0 ? 0 : rem);
};
tick();
countdownRef.current = setInterval(tick, 200);
}
function stopCountdown() {
if (countdownRef.current) { clearInterval(countdownRef.current); countdownRef.current = null; }
setCountdown(null);
}
// Polling
function stopPolling() {
if (pollRef.current) { clearInterval(pollRef.current); pollRef.current = null; }
}
async function poll(code: string, pid: string) {
try {
const res = await fetch(`/api/rooms/${code}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "heartbeat", playerId: pid }),
});
if (res.ok) setRoom((await res.json() as { room: Room }).room);
} catch { /* ignore */ }
}
function startPolling(code: string, pid: string) {
stopPolling();
pollRef.current = setInterval(() => poll(code, pid), POLL_INTERVAL);
}
// Phase sync
useEffect(() => {
if (!room) return;
const prevPhase = prevPhaseRef.current;
const prevRound = prevRoundRef.current;
prevPhaseRef.current = room.phase;
prevRoundRef.current = room.round;
if (room.phase === "countdown" && prevPhase !== "countdown") {
setHtml(""); setLoadError(null);
clicksRef.current = 0; setClicksDisplay(0);
timerStartedRef.current = false;
timer.reset();
startCountdown(room.countdownStart ?? Date.now());
}
if (room.phase === "playing" && prevPhase !== "playing") {
stopCountdown();
historyRef.current = [room.startArticle];
setHistory([room.startArticle]);
loadArticle(room.startArticle);
}
if (room.phase === "results" && prevPhase !== "results") {
timer.stop();
}
if (room.round !== prevRound && room.phase === "playing") {
loadArticle(room.startArticle);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [room]);
// Countdown -> playing transition
useEffect(() => {
if (!room || room.phase !== "countdown" || !playerId) return;
if (Date.now() - (room.countdownStart ?? 0) >= COUNTDOWN_DURATION) {
fetch(`/api/rooms/${room.code}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "play", playerId }),
}).then((r) => r.json()).then((d) => {
if ((d as { room: Room }).room) setRoom((d as { room: Room }).room);
}).catch(() => {});
}
}, [countdown, room, playerId]);
// Navigation
const navigate = useCallback(async (t: string) => {
if (!room || !playerId || loadingRef.current || room.phase !== "playing") return;
clicksRef.current += 1; setClicksDisplay(clicksRef.current);
if (!timerStartedRef.current) { timer.start(); timerStartedRef.current = true; }
const canonical = await loadArticle(t);
if (!canonical) return;
const newHistory = [...historyRef.current, canonical];
historyRef.current = newHistory;
setHistory(newHistory);
window.scrollTo({ top: 0, behavior: "smooth" });
try {
const res = await fetch(`/api/rooms/${room.code}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "navigate", playerId, article: canonical }),
});
if (res.ok) setRoom((await res.json() as { room: Room }).room);
} catch { /* on continue localement */ }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [room, playerId]);
// Room actions
async function createRoom(playerName: string): Promise<{ error?: string }> {
const res = await fetch("/api/rooms", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ playerName }),
});
const data = await res.json() as { room?: Room; playerId?: string; error?: string };
if (!res.ok) return { error: data.error ?? "Erreur" };
setRoom(data.room!); setPlayerId(data.playerId!);
startPolling(data.room!.code, data.playerId!);
saveSession({ screen: "lobby", multiRoomCode: data.room!.code, multiPlayerId: data.playerId!, playerName });
return {};
}
async function joinRoom(playerName: string, code: string): Promise<{ error?: string }> {
const res = await fetch(`/api/rooms/${code}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "join", playerName }),
});
const data = await res.json() as { room?: Room; playerId?: string; error?: string };
if (!res.ok) return { error: data.error ?? "Impossible de rejoindre" };
setRoom(data.room!); setPlayerId(data.playerId!);
startPolling(data.room!.code, data.playerId!);
saveSession({ screen: "lobby", multiRoomCode: data.room!.code, multiPlayerId: data.playerId!, playerName });
return {};
}
async function startGame(): Promise<{ error?: string }> {
if (!room || !playerId) return {};
const puzzle = await pickTwoArticles();
const res = await fetch(`/api/rooms/${room.code}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "start", playerId, startArticle: puzzle.start, targetArticle: puzzle.target }),
});
const data = await res.json() as { room?: Room; error?: string };
if (!res.ok) return { error: data.error ?? "Erreur" };
prefetchArticle(puzzle.start);
setRoom(data.room!);
return {};
}
async function nextRound() {
if (!room || !playerId) return;
const res = await fetch(`/api/rooms/${room.code}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "nextRound", playerId }),
});
if (res.ok) setRoom((await res.json() as { room: Room }).room);
}
async function resetGame() {
if (!room || !playerId) return;
const res = await fetch(`/api/rooms/${room.code}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "resetGame", playerId }),
});
if (res.ok) setRoom((await res.json() as { room: Room }).room);
}
function leave() {
stopPolling(); stopCountdown(); timer.stop();
setRoom(null); setPlayerId(null);
setHtml(""); setTitle("");
setHistory([]); historyRef.current = [];
clicksRef.current = 0; setClicksDisplay(0);
timerStartedRef.current = false;
clearSession();
}
// Restore session depuis sessionStorage (F5)
async function restore(code: string, pid: string): Promise<boolean> {
try {
const res = await fetch(`/api/rooms/${code}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "heartbeat", playerId: pid }),
});
if (!res.ok) return false;
const data = await res.json() as { room: Room };
setRoom(data.room);
setPlayerId(pid);
startPolling(code, pid);
return true;
} catch {
return false;
}
}
return {
room, playerId, html, title, loading, loadError,
history, clicks: clicksDisplay, elapsed: timer.elapsed, countdown,
createRoom, joinRoom, startGame, nextRound, resetGame, leave, navigate, restore,
retryLoad: () => title && loadArticle(title),
};
}
+156
View File
@@ -0,0 +1,156 @@
"use client";
import { useState, useCallback, useEffect, useRef } from "react";
import { fetchArticle, pickTwoArticles, normalizeTitle } from "./wiki";
import { useTimer } from "./useTimer";
import { saveSession, clearSession } from "./session";
import type { Puzzle } from "./types";
export type SoloPhase = "setup" | "playing" | "won";
export function useSoloGame() {
const timer = useTimer();
const clicksRef = useRef(0);
const [clicksDisplay, setClicksDisplay] = useState(0);
const pathRef = useRef<string[]>([]);
const [history, setHistory] = useState<string[]>([]);
const timerStartedRef = useRef(false);
const gameEndedRef = useRef(false);
const loadingRef = useRef(false);
const [html, setHtml] = useState("");
const [title, setTitle] = useState("");
const [loading, setLoading] = useState(false);
const [phase, setPhase] = useState<SoloPhase>("setup");
const [puzzle, setPuzzle] = useState<Puzzle | null>(null);
const [loadError, setLoadError] = useState<string | null>(null);
async function loadArticle(t: string): Promise<string | null> {
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; setClicksDisplay(0);
pathRef.current = []; setHistory([]);
timerStartedRef.current = false;
gameEndedRef.current = false;
timer.reset();
setLoadError(null);
const p = await pickTwoArticles();
setPuzzle(p);
const canonical = await loadArticle(p.start);
if (!canonical) return;
pathRef.current = [canonical];
setHistory([canonical]);
setPhase("playing");
saveSession({ screen: "solo", soloPuzzle: p, soloHistory: [canonical], soloClicks: 0 });
}
const navigate = useCallback(async (t: string) => {
if (loadingRef.current || gameEndedRef.current) return;
clicksRef.current += 1;
setClicksDisplay(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.target)) {
timer.stop();
gameEndedRef.current = true;
setPhase("won");
clearSession();
} else {
saveSession({ screen: "solo", soloPuzzle: puzzle ?? undefined, soloHistory: newPath, soloClicks: clicksRef.current });
}
// 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;
setClicksDisplay(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
}, []);
function reset() {
timer.reset();
clicksRef.current = 0; setClicksDisplay(0);
pathRef.current = []; setHistory([]);
timerStartedRef.current = false;
gameEndedRef.current = false;
setHtml(""); setTitle("");
setPuzzle(null);
setPhase("setup");
setLoadError(null);
clearSession();
}
// Expose une fonction pour restaurer une session sauvegardée
async function restore(savedPuzzle: Puzzle, savedHistory: string[], savedClicks: number) {
setPuzzle(savedPuzzle);
clicksRef.current = savedClicks; setClicksDisplay(savedClicks);
const lastTitle = savedHistory[savedHistory.length - 1];
const canonical = await loadArticle(lastTitle);
if (!canonical) return false;
pathRef.current = savedHistory;
setHistory(savedHistory);
timerStartedRef.current = false;
gameEndedRef.current = false;
setPhase("playing");
return true;
}
return {
phase, puzzle, html, title, loading, loadError, history,
clicks: clicksDisplay, elapsed: timer.elapsed,
canGoBack: pathRef.current.length > 1,
start, navigate, goBack, reset, restore,
retryLoad: () => title && loadArticle(title),
};
}
export function useSoloKeyboard(
active: boolean,
goBack: () => void,
) {
useEffect(() => {
if (!active) return;
const onKey = (e: KeyboardEvent) => {
if (
e.key === "Backspace" &&
!(e.target instanceof HTMLInputElement) &&
!(e.target instanceof HTMLTextAreaElement)
) {
e.preventDefault();
goBack();
}
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [active, goBack]);
}
+45
View File
@@ -0,0 +1,45 @@
import { useState, useEffect, useCallback, useRef } from "react";
export function useTimer() {
const [elapsed, setElapsed] = useState(0);
const startTimeRef = useRef<number | null>(null);
const rafRef = useRef<number | null>(null);
const startRef = useRef(() => {
startTimeRef.current = performance.now();
function tick() {
if (startTimeRef.current !== null) {
setElapsed((performance.now() - startTimeRef.current) / 1000);
rafRef.current = requestAnimationFrame(tick);
}
}
rafRef.current = requestAnimationFrame(tick);
});
const stopRef = useRef((): number => {
let final = 0;
if (startTimeRef.current !== null) {
final = (performance.now() - startTimeRef.current) / 1000;
setElapsed(final);
}
if (rafRef.current !== null) {
cancelAnimationFrame(rafRef.current);
rafRef.current = null;
}
startTimeRef.current = null;
return final;
});
const resetRef = useRef(() => {
stopRef.current();
setElapsed(0);
});
const start = useCallback(() => startRef.current(), []);
const stop = useCallback(() => stopRef.current(), []);
const reset = useCallback(() => resetRef.current(), []);
useEffect(() => () => { if (rafRef.current !== null) cancelAnimationFrame(rafRef.current); }, []);
return { elapsed, start, stop, reset };
}
+113
View File
@@ -0,0 +1,113 @@
import type { WikiArticle, Puzzle } from "./types";
import { getFallbackPuzzle } from "./puzzles";
const WIKI_API_BASE = "https://fr.wikipedia.org/w/api.php";
const MIN_ARTICLE_BYTES = 10000;
const BAD_TITLE_PREFIXES = ["Liste de", "Liste des", "Index de", "Portail:"];
const BAD_TITLE_SUFFIXES = ["(homonymie)", "(disambiguation)"];
// Cache de promesses module-level
const articleCache = new Map<string, Promise<WikiArticle | null>>();
function doFetchArticle(title: string): Promise<WikiArticle | null> {
const params = new URLSearchParams({
action: "parse",
page: title,
format: "json",
origin: "*",
prop: "text|displaytitle",
disableeditsection: "1",
redirects: "1",
});
return fetch(`${WIKI_API_BASE}?${params}`)
.then((res) => {
if (!res.ok) throw new Error("Erreur reseau");
return res.json();
})
.then((data): WikiArticle => {
if (data.error) throw new Error(data.error.info ?? "Article introuvable");
return {
html: data.parse.text["*"] as string,
title: data.parse.title as string,
};
})
.catch((err) => {
articleCache.delete(title);
throw err;
});
}
function getCachedArticle(title: string): Promise<WikiArticle | null> {
if (!articleCache.has(title)) {
articleCache.set(title, doFetchArticle(title));
}
return articleCache.get(title)!;
}
export function prefetchArticle(title: string): void {
getCachedArticle(title).catch(() => {});
}
export async function fetchArticle(title: string): Promise<WikiArticle | null> {
try {
return await getCachedArticle(title);
} catch {
return null;
}
}
interface WikiPageInfo {
title: string;
length: number;
}
function isGoodArticle(page: WikiPageInfo): boolean {
const t = page.title;
return (
page.length >= MIN_ARTICLE_BYTES &&
!BAD_TITLE_PREFIXES.some((p) => t.startsWith(p)) &&
!BAD_TITLE_SUFFIXES.some((s) => t.endsWith(s))
);
}
async function fetchRandomCandidates(): Promise<string[]> {
const params = new URLSearchParams({
action: "query",
generator: "random",
grnnamespace: "0",
grnlimit: "50",
grnfilterredir: "nonredirects",
prop: "info",
format: "json",
origin: "*",
});
const res = await fetch(`${WIKI_API_BASE}?${params}`);
if (!res.ok) throw new Error("Erreur reseau");
const data = await res.json() as { query: { pages: Record<string, WikiPageInfo> } };
return Object.values(data.query.pages)
.filter(isGoodArticle)
.map((p) => p.title);
}
export async function pickTwoArticles(): Promise<Puzzle> {
try {
const collected: string[] = [];
for (let i = 0; i < 3; i++) {
const batch = await fetchRandomCandidates();
for (const title of batch) {
if (!collected.includes(title)) collected.push(title);
if (collected.length >= 2) return { start: collected[0], target: collected[1] };
}
}
} catch {
// Fallback si l'API est indisponible
}
return getFallbackPuzzle();
}
export function normalizeTitle(s: string): string {
return decodeURIComponent(s).replace(/_/g, " ").toLowerCase().trim();
}
export const POLL_INTERVAL = 2000;
export const COUNTDOWN_DURATION = 3000;