From 2bf5e6e700b7d0201b168e0c49d1576524441a90 Mon Sep 17 00:00:00 2001 From: Maix0 Date: Tue, 6 Jan 2026 23:44:02 +0100 Subject: [PATCH] feat(ttt): match history done --- Makefile | 7 +- .../api/generated/.openapi-generator/FILES | 5 + .../src/api/generated/apis/OpenapiOtherApi.ts | 64 ++++++ .../generated/models/TttHistory200Response.ts | 110 ++++++++++ .../models/TttHistory200ResponsePayload.ts | 74 +++++++ .../TttHistory200ResponsePayloadDataInner.ts | 122 +++++++++++ ...story200ResponsePayloadDataInnerPlayerX.ts | 75 +++++++ .../generated/models/TttHistory404Response.ts | 93 +++++++++ frontend/src/api/generated/models/index.ts | 5 + frontend/src/pages/index.ts | 1 + frontend/src/pages/tttHistory/tttHistory.html | 18 ++ frontend/src/pages/tttHistory/tttHistory.ts | 89 ++++++++ src/@shared/src/database/init.sql | 9 +- src/@shared/src/database/mixin/tictactoe.ts | 81 +++++++- src/openapi.json | 196 ++++++++++++++++++ src/tic-tac-toe/openapi.json | 196 +++++++++++++++++- src/tic-tac-toe/src/routes/tttHistory.ts | 58 ++++++ 17 files changed, 1185 insertions(+), 18 deletions(-) create mode 100644 frontend/src/api/generated/models/TttHistory200Response.ts create mode 100644 frontend/src/api/generated/models/TttHistory200ResponsePayload.ts create mode 100644 frontend/src/api/generated/models/TttHistory200ResponsePayloadDataInner.ts create mode 100644 frontend/src/api/generated/models/TttHistory200ResponsePayloadDataInnerPlayerX.ts create mode 100644 frontend/src/api/generated/models/TttHistory404Response.ts create mode 100644 frontend/src/pages/tttHistory/tttHistory.html create mode 100644 frontend/src/pages/tttHistory/tttHistory.ts create mode 100644 src/tic-tac-toe/src/routes/tttHistory.ts diff --git a/Makefile b/Makefile index 0367b2d..b43179c 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ prune: logs: @$(MAKE) --no-print-directory -f ./Docker.mk logs -sqlite: +sql: docker compose exec auth apk add sqlite -docker compose exec -it auth sqlite3 /volumes/database/database.db @@ -125,11 +125,6 @@ npm@openapi: openapi.jar openapi.jar: wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.15.0/openapi-generator-cli-7.15.0.jar -O ./openapi.jar -# this convert the .dbml file to an actual sql file that SQLite can handle :) -sql: - @echo "if the command isn't found, contact maieul :)" - dbml_sqlite -t -f -w ./src/@shared/src/database/init.sql ./src/@shared/src/database/init.dbml - tmux: @tmux new-session -d -s $(PROJECT) @tmux send-keys -t $(PROJECT):0 'vim' C-m diff --git a/frontend/src/api/generated/.openapi-generator/FILES b/frontend/src/api/generated/.openapi-generator/FILES index 8ab2789..7bf4f15 100644 --- a/frontend/src/api/generated/.openapi-generator/FILES +++ b/frontend/src/api/generated/.openapi-generator/FILES @@ -66,5 +66,10 @@ models/StatusOtp200ResponseAnyOf1.ts models/StatusOtp200ResponseAnyOfPayload.ts models/StatusOtp401Response.ts models/StatusOtp500Response.ts +models/TttHistory200Response.ts +models/TttHistory200ResponsePayload.ts +models/TttHistory200ResponsePayloadDataInner.ts +models/TttHistory200ResponsePayloadDataInnerPlayerX.ts +models/TttHistory404Response.ts models/index.ts runtime.ts diff --git a/frontend/src/api/generated/apis/OpenapiOtherApi.ts b/frontend/src/api/generated/apis/OpenapiOtherApi.ts index 7d116bf..9762acd 100644 --- a/frontend/src/api/generated/apis/OpenapiOtherApi.ts +++ b/frontend/src/api/generated/apis/OpenapiOtherApi.ts @@ -63,6 +63,8 @@ import type { StatusOtp200Response, StatusOtp401Response, StatusOtp500Response, + TttHistory200Response, + TttHistory404Response, } from '../models/index'; import { AllowGuestMessage200ResponseFromJSON, @@ -161,6 +163,10 @@ import { StatusOtp401ResponseToJSON, StatusOtp500ResponseFromJSON, StatusOtp500ResponseToJSON, + TttHistory200ResponseFromJSON, + TttHistory200ResponseToJSON, + TttHistory404ResponseFromJSON, + TttHistory404ResponseToJSON, } from '../models/index'; export interface ChangeDescOperationRequest { @@ -199,6 +205,10 @@ export interface SigninRequest { loginRequest: LoginRequest; } +export interface TttHistoryRequest { + user: string; +} + /** * */ @@ -1027,4 +1037,58 @@ export class OpenapiOtherApi extends runtime.BaseAPI { return await response.value(); } + /** + */ + async tttHistoryRaw(requestParameters: TttHistoryRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { + if (requestParameters['user'] == null) { + throw new runtime.RequiredError( + 'user', + 'Required parameter "user" was null or undefined when calling tttHistory().' + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + + let urlPath = `/api/ttt/history/{user}`; + urlPath = urlPath.replace(`{${"user"}}`, encodeURIComponent(String(requestParameters['user']))); + + const response = await this.request({ + path: urlPath, + method: 'GET', + headers: headerParameters, + query: queryParameters, + }, initOverrides); + + // CHANGED: Handle all status codes defined in the OpenAPI spec, not just 2xx responses + // This allows typed access to error responses (4xx, 5xx) and other status codes. + // The code routes responses based on the actual HTTP status code and returns + // appropriately typed ApiResponse wrappers for each status code. + if (response.status === 200) { + // Object response for status 200 + return new runtime.JSONApiResponse(response, (jsonValue) => TttHistory200ResponseFromJSON(jsonValue)); + } + if (response.status === 401) { + // Object response for status 401 + return new runtime.JSONApiResponse(response, (jsonValue) => StatusOtp401ResponseFromJSON(jsonValue)); + } + if (response.status === 404) { + // Object response for status 404 + return new runtime.JSONApiResponse(response, (jsonValue) => TttHistory404ResponseFromJSON(jsonValue)); + } + // CHANGED: Throw error if status code is not handled by any of the defined responses + // This ensures all code paths return a value and provides clear error messages for unexpected status codes + // Only throw if responses were defined but none matched the actual status code + throw new runtime.ResponseError(response, `Unexpected status code: ${response.status}. Expected one of: 200, 401, 404`); + } + + /** + */ + async tttHistory(requestParameters: TttHistoryRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { + const response = await this.tttHistoryRaw(requestParameters, initOverrides); + return await response.value(); + } + } diff --git a/frontend/src/api/generated/models/TttHistory200Response.ts b/frontend/src/api/generated/models/TttHistory200Response.ts new file mode 100644 index 0000000..310b06d --- /dev/null +++ b/frontend/src/api/generated/models/TttHistory200Response.ts @@ -0,0 +1,110 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { TttHistory200ResponsePayload } from './TttHistory200ResponsePayload'; +import { + TttHistory200ResponsePayloadFromJSON, + TttHistory200ResponsePayloadFromJSONTyped, + TttHistory200ResponsePayloadToJSON, + TttHistory200ResponsePayloadToJSONTyped, +} from './TttHistory200ResponsePayload'; + +/** + * + * @export + * @interface TttHistory200Response + */ +export interface TttHistory200Response { + /** + * + * @type {string} + * @memberof TttHistory200Response + */ + kind: TttHistory200ResponseKindEnum; + /** + * + * @type {string} + * @memberof TttHistory200Response + */ + msg: TttHistory200ResponseMsgEnum; + /** + * + * @type {TttHistory200ResponsePayload} + * @memberof TttHistory200Response + */ + payload: TttHistory200ResponsePayload; +} + + +/** + * @export + */ +export const TttHistory200ResponseKindEnum = { + Success: 'success' +} as const; +export type TttHistory200ResponseKindEnum = typeof TttHistory200ResponseKindEnum[keyof typeof TttHistory200ResponseKindEnum]; + +/** + * @export + */ +export const TttHistory200ResponseMsgEnum = { + TtthistorySuccess: 'ttthistory.success' +} as const; +export type TttHistory200ResponseMsgEnum = typeof TttHistory200ResponseMsgEnum[keyof typeof TttHistory200ResponseMsgEnum]; + + +/** + * Check if a given object implements the TttHistory200Response interface. + */ +export function instanceOfTttHistory200Response(value: object): value is TttHistory200Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + if (!('payload' in value) || value['payload'] === undefined) return false; + return true; +} + +export function TttHistory200ResponseFromJSON(json: any): TttHistory200Response { + return TttHistory200ResponseFromJSONTyped(json, false); +} + +export function TttHistory200ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): TttHistory200Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + 'payload': TttHistory200ResponsePayloadFromJSON(json['payload']), + }; +} + +export function TttHistory200ResponseToJSON(json: any): TttHistory200Response { + return TttHistory200ResponseToJSONTyped(json, false); +} + +export function TttHistory200ResponseToJSONTyped(value?: TttHistory200Response | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'kind': value['kind'], + 'msg': value['msg'], + 'payload': TttHistory200ResponsePayloadToJSON(value['payload']), + }; +} + diff --git a/frontend/src/api/generated/models/TttHistory200ResponsePayload.ts b/frontend/src/api/generated/models/TttHistory200ResponsePayload.ts new file mode 100644 index 0000000..ed920d0 --- /dev/null +++ b/frontend/src/api/generated/models/TttHistory200ResponsePayload.ts @@ -0,0 +1,74 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { TttHistory200ResponsePayloadDataInner } from './TttHistory200ResponsePayloadDataInner'; +import { + TttHistory200ResponsePayloadDataInnerFromJSON, + TttHistory200ResponsePayloadDataInnerFromJSONTyped, + TttHistory200ResponsePayloadDataInnerToJSON, + TttHistory200ResponsePayloadDataInnerToJSONTyped, +} from './TttHistory200ResponsePayloadDataInner'; + +/** + * + * @export + * @interface TttHistory200ResponsePayload + */ +export interface TttHistory200ResponsePayload { + /** + * + * @type {Array} + * @memberof TttHistory200ResponsePayload + */ + data: Array; +} + +/** + * Check if a given object implements the TttHistory200ResponsePayload interface. + */ +export function instanceOfTttHistory200ResponsePayload(value: object): value is TttHistory200ResponsePayload { + if (!('data' in value) || value['data'] === undefined) return false; + return true; +} + +export function TttHistory200ResponsePayloadFromJSON(json: any): TttHistory200ResponsePayload { + return TttHistory200ResponsePayloadFromJSONTyped(json, false); +} + +export function TttHistory200ResponsePayloadFromJSONTyped(json: any, ignoreDiscriminator: boolean): TttHistory200ResponsePayload { + if (json == null) { + return json; + } + return { + + 'data': ((json['data'] as Array).map(TttHistory200ResponsePayloadDataInnerFromJSON)), + }; +} + +export function TttHistory200ResponsePayloadToJSON(json: any): TttHistory200ResponsePayload { + return TttHistory200ResponsePayloadToJSONTyped(json, false); +} + +export function TttHistory200ResponsePayloadToJSONTyped(value?: TttHistory200ResponsePayload | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'data': ((value['data'] as Array).map(TttHistory200ResponsePayloadDataInnerToJSON)), + }; +} + diff --git a/frontend/src/api/generated/models/TttHistory200ResponsePayloadDataInner.ts b/frontend/src/api/generated/models/TttHistory200ResponsePayloadDataInner.ts new file mode 100644 index 0000000..80b2ee8 --- /dev/null +++ b/frontend/src/api/generated/models/TttHistory200ResponsePayloadDataInner.ts @@ -0,0 +1,122 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +import type { TttHistory200ResponsePayloadDataInnerPlayerX } from './TttHistory200ResponsePayloadDataInnerPlayerX'; +import { + TttHistory200ResponsePayloadDataInnerPlayerXFromJSON, + TttHistory200ResponsePayloadDataInnerPlayerXFromJSONTyped, + TttHistory200ResponsePayloadDataInnerPlayerXToJSON, + TttHistory200ResponsePayloadDataInnerPlayerXToJSONTyped, +} from './TttHistory200ResponsePayloadDataInnerPlayerX'; + +/** + * + * @export + * @interface TttHistory200ResponsePayloadDataInner + */ +export interface TttHistory200ResponsePayloadDataInner { + /** + * gameId + * @type {string} + * @memberof TttHistory200ResponsePayloadDataInner + */ + gameId: string; + /** + * + * @type {TttHistory200ResponsePayloadDataInnerPlayerX} + * @memberof TttHistory200ResponsePayloadDataInner + */ + playerX: TttHistory200ResponsePayloadDataInnerPlayerX; + /** + * + * @type {TttHistory200ResponsePayloadDataInnerPlayerX} + * @memberof TttHistory200ResponsePayloadDataInner + */ + playerO: TttHistory200ResponsePayloadDataInnerPlayerX; + /** + * + * @type {string} + * @memberof TttHistory200ResponsePayloadDataInner + */ + date: string; + /** + * + * @type {string} + * @memberof TttHistory200ResponsePayloadDataInner + */ + outcome: TttHistory200ResponsePayloadDataInnerOutcomeEnum; +} + + +/** + * @export + */ +export const TttHistory200ResponsePayloadDataInnerOutcomeEnum = { + WinX: 'winX', + WinO: 'winO', + Other: 'other' +} as const; +export type TttHistory200ResponsePayloadDataInnerOutcomeEnum = typeof TttHistory200ResponsePayloadDataInnerOutcomeEnum[keyof typeof TttHistory200ResponsePayloadDataInnerOutcomeEnum]; + + +/** + * Check if a given object implements the TttHistory200ResponsePayloadDataInner interface. + */ +export function instanceOfTttHistory200ResponsePayloadDataInner(value: object): value is TttHistory200ResponsePayloadDataInner { + if (!('gameId' in value) || value['gameId'] === undefined) return false; + if (!('playerX' in value) || value['playerX'] === undefined) return false; + if (!('playerO' in value) || value['playerO'] === undefined) return false; + if (!('date' in value) || value['date'] === undefined) return false; + if (!('outcome' in value) || value['outcome'] === undefined) return false; + return true; +} + +export function TttHistory200ResponsePayloadDataInnerFromJSON(json: any): TttHistory200ResponsePayloadDataInner { + return TttHistory200ResponsePayloadDataInnerFromJSONTyped(json, false); +} + +export function TttHistory200ResponsePayloadDataInnerFromJSONTyped(json: any, ignoreDiscriminator: boolean): TttHistory200ResponsePayloadDataInner { + if (json == null) { + return json; + } + return { + + 'gameId': json['gameId'], + 'playerX': TttHistory200ResponsePayloadDataInnerPlayerXFromJSON(json['playerX']), + 'playerO': TttHistory200ResponsePayloadDataInnerPlayerXFromJSON(json['playerO']), + 'date': json['date'], + 'outcome': json['outcome'], + }; +} + +export function TttHistory200ResponsePayloadDataInnerToJSON(json: any): TttHistory200ResponsePayloadDataInner { + return TttHistory200ResponsePayloadDataInnerToJSONTyped(json, false); +} + +export function TttHistory200ResponsePayloadDataInnerToJSONTyped(value?: TttHistory200ResponsePayloadDataInner | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'gameId': value['gameId'], + 'playerX': TttHistory200ResponsePayloadDataInnerPlayerXToJSON(value['playerX']), + 'playerO': TttHistory200ResponsePayloadDataInnerPlayerXToJSON(value['playerO']), + 'date': value['date'], + 'outcome': value['outcome'], + }; +} + diff --git a/frontend/src/api/generated/models/TttHistory200ResponsePayloadDataInnerPlayerX.ts b/frontend/src/api/generated/models/TttHistory200ResponsePayloadDataInnerPlayerX.ts new file mode 100644 index 0000000..97e417f --- /dev/null +++ b/frontend/src/api/generated/models/TttHistory200ResponsePayloadDataInnerPlayerX.ts @@ -0,0 +1,75 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface TttHistory200ResponsePayloadDataInnerPlayerX + */ +export interface TttHistory200ResponsePayloadDataInnerPlayerX { + /** + * + * @type {string} + * @memberof TttHistory200ResponsePayloadDataInnerPlayerX + */ + id: string; + /** + * + * @type {string} + * @memberof TttHistory200ResponsePayloadDataInnerPlayerX + */ + name: string; +} + +/** + * Check if a given object implements the TttHistory200ResponsePayloadDataInnerPlayerX interface. + */ +export function instanceOfTttHistory200ResponsePayloadDataInnerPlayerX(value: object): value is TttHistory200ResponsePayloadDataInnerPlayerX { + if (!('id' in value) || value['id'] === undefined) return false; + if (!('name' in value) || value['name'] === undefined) return false; + return true; +} + +export function TttHistory200ResponsePayloadDataInnerPlayerXFromJSON(json: any): TttHistory200ResponsePayloadDataInnerPlayerX { + return TttHistory200ResponsePayloadDataInnerPlayerXFromJSONTyped(json, false); +} + +export function TttHistory200ResponsePayloadDataInnerPlayerXFromJSONTyped(json: any, ignoreDiscriminator: boolean): TttHistory200ResponsePayloadDataInnerPlayerX { + if (json == null) { + return json; + } + return { + + 'id': json['id'], + 'name': json['name'], + }; +} + +export function TttHistory200ResponsePayloadDataInnerPlayerXToJSON(json: any): TttHistory200ResponsePayloadDataInnerPlayerX { + return TttHistory200ResponsePayloadDataInnerPlayerXToJSONTyped(json, false); +} + +export function TttHistory200ResponsePayloadDataInnerPlayerXToJSONTyped(value?: TttHistory200ResponsePayloadDataInnerPlayerX | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'id': value['id'], + 'name': value['name'], + }; +} + diff --git a/frontend/src/api/generated/models/TttHistory404Response.ts b/frontend/src/api/generated/models/TttHistory404Response.ts new file mode 100644 index 0000000..75d7341 --- /dev/null +++ b/frontend/src/api/generated/models/TttHistory404Response.ts @@ -0,0 +1,93 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * @fastify/swagger + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 9.6.1 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { mapValues } from '../runtime'; +/** + * + * @export + * @interface TttHistory404Response + */ +export interface TttHistory404Response { + /** + * + * @type {string} + * @memberof TttHistory404Response + */ + kind: TttHistory404ResponseKindEnum; + /** + * + * @type {string} + * @memberof TttHistory404Response + */ + msg: TttHistory404ResponseMsgEnum; +} + + +/** + * @export + */ +export const TttHistory404ResponseKindEnum = { + Failure: 'failure' +} as const; +export type TttHistory404ResponseKindEnum = typeof TttHistory404ResponseKindEnum[keyof typeof TttHistory404ResponseKindEnum]; + +/** + * @export + */ +export const TttHistory404ResponseMsgEnum = { + TtthistoryFailureNotfound: 'ttthistory.failure.notfound' +} as const; +export type TttHistory404ResponseMsgEnum = typeof TttHistory404ResponseMsgEnum[keyof typeof TttHistory404ResponseMsgEnum]; + + +/** + * Check if a given object implements the TttHistory404Response interface. + */ +export function instanceOfTttHistory404Response(value: object): value is TttHistory404Response { + if (!('kind' in value) || value['kind'] === undefined) return false; + if (!('msg' in value) || value['msg'] === undefined) return false; + return true; +} + +export function TttHistory404ResponseFromJSON(json: any): TttHistory404Response { + return TttHistory404ResponseFromJSONTyped(json, false); +} + +export function TttHistory404ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): TttHistory404Response { + if (json == null) { + return json; + } + return { + + 'kind': json['kind'], + 'msg': json['msg'], + }; +} + +export function TttHistory404ResponseToJSON(json: any): TttHistory404Response { + return TttHistory404ResponseToJSONTyped(json, false); +} + +export function TttHistory404ResponseToJSONTyped(value?: TttHistory404Response | null, ignoreDiscriminator: boolean = false): any { + if (value == null) { + return value; + } + + return { + + 'kind': value['kind'], + 'msg': value['msg'], + }; +} + diff --git a/frontend/src/api/generated/models/index.ts b/frontend/src/api/generated/models/index.ts index f085a6b..07e430b 100644 --- a/frontend/src/api/generated/models/index.ts +++ b/frontend/src/api/generated/models/index.ts @@ -65,3 +65,8 @@ export * from './StatusOtp200ResponseAnyOf1'; export * from './StatusOtp200ResponseAnyOfPayload'; export * from './StatusOtp401Response'; export * from './StatusOtp500Response'; +export * from './TttHistory200Response'; +export * from './TttHistory200ResponsePayload'; +export * from './TttHistory200ResponsePayloadDataInner'; +export * from './TttHistory200ResponsePayloadDataInnerPlayerX'; +export * from './TttHistory404Response'; diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index b750328..ec82683 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -8,6 +8,7 @@ import './ttt/ttt.ts' import './profile/profile.ts' import './logout/logout.ts' import './pongHistory/pongHistory.ts' +import './tttHistory/tttHistory.ts' // ---- Initial load ---- setTitle(""); diff --git a/frontend/src/pages/tttHistory/tttHistory.html b/frontend/src/pages/tttHistory/tttHistory.html new file mode 100644 index 0000000..0516605 --- /dev/null +++ b/frontend/src/pages/tttHistory/tttHistory.html @@ -0,0 +1,18 @@ +
+
+

+ TicTacToe Match History For + +

+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/pages/tttHistory/tttHistory.ts b/frontend/src/pages/tttHistory/tttHistory.ts new file mode 100644 index 0000000..da03bfa --- /dev/null +++ b/frontend/src/pages/tttHistory/tttHistory.ts @@ -0,0 +1,89 @@ +import { addRoute, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing"; +import page from './tttHistory.html?raw'; +import { isNullish } from "@app/utils"; +import client from "@app/api"; +import { updateUser } from "@app/auth"; + + +function getHHMM(d: Date): string { + let h = d.getHours(); + let m = d.getMinutes(); + return `${h < 9 ? '0' : ''}${h}:${m < 9 ? '0' : ''}${m}` +} + +async function tttHistory(_url: string, args: RouteHandlerParams): Promise { + if (isNullish(args.userid)) + args.userid = 'me'; + let user = await updateUser(); + if (isNullish(user)) { + return { html: ' You aren\'t logged in ' }; + } + + let userInfoRes = await client.getUser({user: args.userid}); + if (userInfoRes.kind !== 'success') + { + return { html: ' You tried to open a game history with no data :(' }; + } + let userInfo = userInfoRes.payload; + let res = await client.tttHistory({ user: args.userid }); + if (res.kind === 'failure' || res.kind === 'notLoggedIn') { + // todo: make a real page on no data + return { html: ' You tried to open a game history with no data :(' }; + } + let games = res.payload.data; + games.reverse(); + + let gameElement = games.map(g => { + let rdate = Date.parse(g.date); + if (Number.isNaN(rdate)) return undefined; + let date = new Date(rdate); + const e = document.createElement('div'); + let color = 'bg-amber-200'; + // maybe we do want local games ? then put the check here :D + // if (!g.local) { + if (true) { + let youwin = false; + + if (g.playerX.id === user.id && g.outcome === 'winX') + youwin = true; + else if (g.playerO.id === user.id && g.outcome === 'winO') + youwin = true; + + if (youwin) + color = 'bg-green-300'; + else + color = 'bg-red-300'; + } + e.className = + 'grid grid-cols-[1fr_auto_1fr] items-center rounded-lg px-4 py-3 ' + color; + + e.innerHTML = ` +
+
${g.playerX.name}
+
${g.outcome === 'winX' ? 'WON' : 'LOST'}
+
+ +
${date.toDateString()}
${getHHMM(date)}
+ +
+
${g.playerO.name}
+
${g.outcome === 'winO' ? 'WON' : 'LOST'}
+
`; + return e; + }).filter(v => !isNullish(v)); + + return { + html: page, postInsert: async (app) => { + if (!app) return; + const matchBox = app.querySelector("#matchList"); + if (!matchBox) return; + gameElement.forEach(c => matchBox.appendChild(c)); + const userBox = app.querySelector("#name"); + if (!userBox) return; + userBox.innerText = userInfo.name; + } + }; +} + +addRoute('/ttt/games', tttHistory); +addRoute('/ttt/games/:userid', tttHistory); diff --git a/src/@shared/src/database/init.sql b/src/@shared/src/database/init.sql index b5d93d1..79170f1 100644 --- a/src/@shared/src/database/init.sql +++ b/src/@shared/src/database/init.sql @@ -21,11 +21,12 @@ CREATE UNIQUE INDEX IF NOT EXISTS idx_blocked_user_pair ON blocked (user, blocke CREATE TABLE IF NOT EXISTS tictactoe ( id TEXT PRIMARY KEY NOT NULL, - player1 TEXT NOT NULL, - player2 TEXT NOT NULL, + time TEXT NOT NULL default (datetime('now')), + playerX TEXT NOT NULL, + playerO TEXT NOT NULL, outcome TEXT NOT NULL, - FOREIGN KEY(player1) REFERENCES user(id), - FOREIGN KEY(player2) REFERENCES user(id) + FOREIGN KEY(playerX) REFERENCES user(id), + FOREIGN KEY(playerO) REFERENCES user(id) ); CREATE TABLE IF NOT EXISTS pong ( diff --git a/src/@shared/src/database/mixin/tictactoe.ts b/src/@shared/src/database/mixin/tictactoe.ts index 85a5494..f7608e7 100644 --- a/src/@shared/src/database/mixin/tictactoe.ts +++ b/src/@shared/src/database/mixin/tictactoe.ts @@ -1,12 +1,19 @@ import UUID from '@shared/utils/uuid'; import type { Database } from './_base'; import { UserId } from './user'; +import { isNullish } from '@shared/utils'; +export type TicTacToeOutcome = 'winX' | 'winO' | 'other'; // describe every function in the object export interface ITicTacToeDb extends Database { - setTTTGameOutcome(this: ITicTacToeDb, id: TTTGameId, player1: UserId, player2: UserId, outcome: string): void, + setTTTGameOutcome(this: ITicTacToeDb, id: TTTGameId, player1: UserId, player2: UserId, outcome: TicTacToeOutcome): void, + getAllTTTGameForUser( + this: ITicTacToeDb, + id: UserId, + ): (TicTacToeGame & { nameX: string, nameO: string })[], }; + export const TicTacToeImpl: Omit = { /** * @brief Write the outcome of the specified game to the database. @@ -14,21 +21,81 @@ export const TicTacToeImpl: Omit = { * @param gameId The game we want to write the outcome of. * */ - setTTTGameOutcome(this: ITicTacToeDb, id: TTTGameId, player1: UserId, player2: UserId, outcome: string): void { + setTTTGameOutcome(this: ITicTacToeDb, id: TTTGameId, playerX: UserId, playerO: UserId, outcome: TicTacToeOutcome): void { // Find a way to retrieve the outcome of the game. - this.prepare('INSERT INTO tictactoe (id, player1, player2, outcome) VALUES (@id, @player1, @player2, @outcome)').run({ id, player1, player2, outcome }); + this.prepare('INSERT INTO tictactoe (id, playerX, playerO, outcome) VALUES (@id, @playerX, @playerO, @outcome)').run({ id, playerX, playerO, outcome }); }, + + getAllTTTGameForUser( + this: ITicTacToeDb, + id: UserId, + ): (TicTacToeGame & { nameX: string, nameO: string })[] { + const q = this.prepare(` + SELECT + tictactoe.*, + userX.name AS nameX, + userO.name AS nameO + FROM tictactoe + INNER JOIN user AS userX + ON tictactoe.playerX = userX.id + INNER JOIN user AS userO + ON tictactoe.playerO = userO.id + WHERE + tictactoe.playerX = @id + OR tictactoe.playerO = @id; + `); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return q.all({ id }).map((s: any) => { + const g: (TicTacToeGame & { nameX?: string, nameO?: string }) | undefined = TicTacToeGameFromRow(s); + if (isNullish(g)) return undefined; + g.nameX = s.nameX; + g.nameO = s.nameO; + if (isNullish(g.nameO) || isNullish(g.nameO)) return undefined; + return g as TicTacToeGame & { nameX: string, nameO: string }; + }).filter(v => !isNullish(v)); + } }; -export type TTTGameId = UUID & { readonly __brand: unique symbol }; +export type TTTGameId = UUID & { readonly __uuid: unique symbol }; export type TicTacToeGame = { readonly id: TTTGameId; - readonly player1: UserId; - readonly player2: UserId; - readonly outcome: string; + readonly time: Date; + readonly playerX: UserId; + readonly playerO: UserId; + readonly outcome: TicTacToeOutcome; }; +type TicTacToeGameTable = { + id: string; + time: string; + playerX: UserId; + playerO: UserId; + outcome: string; +}; + +function TicTacToeGameFromRow(r: Partial | undefined): TicTacToeGame | undefined { + if (isNullish(r)) return undefined; + if (isNullish(r.id)) return undefined; + if (isNullish(r.playerX)) return undefined; + if (isNullish(r.playerO)) return undefined; + if (isNullish(r.outcome)) return undefined; + if (isNullish(r.time)) return undefined; + + if (r.outcome !== 'winX' && r.outcome !== 'winO' && r.outcome !== 'other') return undefined; + const date = Date.parse(r.time); + if (Number.isNaN(date)) return undefined; + + + return { + id: r.id as TTTGameId, + playerX: r.playerX, + playerO: r.playerO, + outcome: r.outcome, + time: new Date(date), + }; +} + // this function will be able to be called from everywhere // export async function freeFloatingExportedFunction(): Promise { // return false; diff --git a/src/openapi.json b/src/openapi.json index 8b87c61..f2e86b9 100644 --- a/src/openapi.json +++ b/src/openapi.json @@ -1917,6 +1917,202 @@ ] } }, + "/api/ttt/history/{user}": { + "get": { + "operationId": "tttHistory", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "user", + "required": true, + "description": "'me' | " + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg", + "payload" + ], + "properties": { + "kind": { + "enum": [ + "success" + ] + }, + "msg": { + "enum": [ + "ttthistory.success" + ] + }, + "payload": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "gameId", + "playerX", + "playerO", + "date", + "outcome" + ], + "properties": { + "gameId": { + "type": "string", + "description": "gameId" + }, + "playerX": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "playerO": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "date": { + "type": "string" + }, + "outcome": { + "enum": [ + "winX", + "winO", + "other" + ] + } + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "notLoggedIn" + ] + }, + "msg": { + "enum": [ + "auth.noCookie", + "auth.invalidKind", + "auth.noUser", + "auth.invalid" + ] + } + } + }, + { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "notLoggedIn" + ] + }, + "msg": { + "enum": [ + "auth.noCookie", + "auth.invalidKind", + "auth.noUser", + "auth.invalid" + ] + } + } + } + ] + } + } + } + }, + "404": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "failure" + ] + }, + "msg": { + "enum": [ + "ttthistory.failure.notfound" + ] + } + } + } + } + } + } + }, + "tags": [ + "openapi_other" + ] + } + }, "/api/pong/history/{user}": { "get": { "operationId": "pongHistory", diff --git a/src/tic-tac-toe/openapi.json b/src/tic-tac-toe/openapi.json index 38cd725..7adfcef 100644 --- a/src/tic-tac-toe/openapi.json +++ b/src/tic-tac-toe/openapi.json @@ -7,7 +7,201 @@ "components": { "schemas": {} }, - "paths": {}, + "paths": { + "/api/ttt/history/{user}": { + "get": { + "operationId": "tttHistory", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "user", + "required": true, + "description": "'me' | " + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg", + "payload" + ], + "properties": { + "kind": { + "enum": [ + "success" + ] + }, + "msg": { + "enum": [ + "ttthistory.success" + ] + }, + "payload": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "required": [ + "gameId", + "playerX", + "playerO", + "date", + "outcome" + ], + "properties": { + "gameId": { + "type": "string", + "description": "gameId" + }, + "playerX": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "playerO": { + "type": "object", + "required": [ + "id", + "name" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "date": { + "type": "string" + }, + "outcome": { + "enum": [ + "winX", + "winO", + "other" + ] + } + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "notLoggedIn" + ] + }, + "msg": { + "enum": [ + "auth.noCookie", + "auth.invalidKind", + "auth.noUser", + "auth.invalid" + ] + } + } + }, + { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "notLoggedIn" + ] + }, + "msg": { + "enum": [ + "auth.noCookie", + "auth.invalidKind", + "auth.noUser", + "auth.invalid" + ] + } + } + } + ] + } + } + } + }, + "404": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "kind", + "msg" + ], + "properties": { + "kind": { + "enum": [ + "failure" + ] + }, + "msg": { + "enum": [ + "ttthistory.failure.notfound" + ] + } + } + } + } + } + } + } + } + } + }, "servers": [ { "url": "https://local.maix.me:8888", diff --git a/src/tic-tac-toe/src/routes/tttHistory.ts b/src/tic-tac-toe/src/routes/tttHistory.ts new file mode 100644 index 0000000..6367cd6 --- /dev/null +++ b/src/tic-tac-toe/src/routes/tttHistory.ts @@ -0,0 +1,58 @@ +import { UserId } from '@shared/database/mixin/user'; +import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils'; +import { FastifyPluginAsync } from 'fastify'; +import { Static, Type } from 'typebox'; + +const TTTHistoryParams = Type.Object({ + user: Type.String({ description: '\'me\' | ' }), +}); + +type TTTHistoryParams = Static; + +const TTTHistoryResponse = { + '200': typeResponse('success', 'ttthistory.success', { + data: Type.Array( + Type.Object({ + gameId: Type.String({ description: 'gameId' }), + playerX: Type.Object({id: Type.String(), name: Type.String()}), + playerO: Type.Object({id: Type.String(), name: Type.String()}), + date: Type.String(), + outcome: Type.Enum(['winX', 'winO', 'other']), + }), + ), + }), + '404': typeResponse('failure', 'ttthistory.failure.notfound'), +}; +type TTTHistoryResponse = MakeStaticResponse; + +const route: FastifyPluginAsync = async (fastify): Promise => { + fastify.get<{ Params: TTTHistoryParams }>( + '/api/ttt/history/:user', + { + schema: { + params: TTTHistoryParams, + response: TTTHistoryResponse, + operationId: 'tttHistory', + }, + config: { requireAuth: true }, + }, + async function(req, res) { + if (req.params.user === 'me') { req.params.user = req.authUser!.id; } + const user = this.db.getUser(req.params.user); + if (isNullish(user)) { return res.makeResponse(404, 'failure', 'ttthistory.failure.notfound'); } + const data = this.db.getAllTTTGameForUser(req.params.user as UserId); + if (isNullish(data)) { return res.makeResponse(404, 'failure', 'ttthistory.failure.notfound'); } + + return res.makeResponse(200, 'success', 'ttthistory.success', { + data: data.map(v => ({ + gameId: v.id, + playerX: { id: v.playerX, name: v.nameX }, + playerO: { id: v.playerO, name: v.nameO }, + date: v.time.toString(), + outcome: v.outcome, + })), + }); + }, + ); +}; +export default route;