From aca2dbd070a72e5e44bc5140e845dd465c4781b9 Mon Sep 17 00:00:00 2001 From: Maieul BOYER Date: Tue, 13 Jan 2026 14:09:25 +0100 Subject: [PATCH] feat(tour): added tournament history page :D --- .../TournamentData200ResponsePayloadData.ts | 9 + ...urnamentList200ResponsePayloadDataInner.ts | 9 + frontend/src/pages/index.ts | 1 + frontend/src/pages/tourHistory/tourHistory.ts | 156 ++++++++++++++++++ .../src/pages/tourHistory/tourHistoryAll.html | 9 + .../pages/tourHistory/tourHistorySingle.html | 37 +++++ frontend/src/pages/ttt/ttt.css | 2 - src/@shared/src/database/init.sql | 1 + src/@shared/src/database/mixin/tournament.ts | 5 +- src/openapi.json | 8 + src/pong/openapi.json | 8 + src/pong/src/routes/tournamentData.ts | 3 + src/pong/src/routes/tournamentList.ts | 1 + 13 files changed, 245 insertions(+), 4 deletions(-) create mode 100644 frontend/src/pages/tourHistory/tourHistory.ts create mode 100644 frontend/src/pages/tourHistory/tourHistoryAll.html create mode 100644 frontend/src/pages/tourHistory/tourHistorySingle.html diff --git a/frontend/src/api/generated/models/TournamentData200ResponsePayloadData.ts b/frontend/src/api/generated/models/TournamentData200ResponsePayloadData.ts index fac9c54..95cdf59 100644 --- a/frontend/src/api/generated/models/TournamentData200ResponsePayloadData.ts +++ b/frontend/src/api/generated/models/TournamentData200ResponsePayloadData.ts @@ -34,6 +34,12 @@ import { * @interface TournamentData200ResponsePayloadData */ export interface TournamentData200ResponsePayloadData { + /** + * + * @type {number} + * @memberof TournamentData200ResponsePayloadData + */ + playerCount: number; /** * * @type {string} @@ -64,6 +70,7 @@ export interface TournamentData200ResponsePayloadData { * Check if a given object implements the TournamentData200ResponsePayloadData interface. */ export function instanceOfTournamentData200ResponsePayloadData(value: object): value is TournamentData200ResponsePayloadData { + if (!('playerCount' in value) || value['playerCount'] === undefined) return false; if (!('owner' in value) || value['owner'] === undefined) return false; if (!('users' in value) || value['users'] === undefined) return false; if (!('games' in value) || value['games'] === undefined) return false; @@ -81,6 +88,7 @@ export function TournamentData200ResponsePayloadDataFromJSONTyped(json: any, ign } return { + 'playerCount': json['playerCount'], 'owner': json['owner'], 'users': ((json['users'] as Array).map(TournamentData200ResponsePayloadDataUsersInnerFromJSON)), 'games': ((json['games'] as Array).map(PongHistory200ResponsePayloadDataInnerFromJSON)), @@ -99,6 +107,7 @@ export function TournamentData200ResponsePayloadDataToJSONTyped(value?: Tourname return { + 'playerCount': value['playerCount'], 'owner': value['owner'], 'users': ((value['users'] as Array).map(TournamentData200ResponsePayloadDataUsersInnerToJSON)), 'games': ((value['games'] as Array).map(PongHistory200ResponsePayloadDataInnerToJSON)), diff --git a/frontend/src/api/generated/models/TournamentList200ResponsePayloadDataInner.ts b/frontend/src/api/generated/models/TournamentList200ResponsePayloadDataInner.ts index c4ca822..9f96e58 100644 --- a/frontend/src/api/generated/models/TournamentList200ResponsePayloadDataInner.ts +++ b/frontend/src/api/generated/models/TournamentList200ResponsePayloadDataInner.ts @@ -19,6 +19,12 @@ import { mapValues } from '../runtime'; * @interface TournamentList200ResponsePayloadDataInner */ export interface TournamentList200ResponsePayloadDataInner { + /** + * + * @type {number} + * @memberof TournamentList200ResponsePayloadDataInner + */ + playerCount: number; /** * * @type {string} @@ -43,6 +49,7 @@ export interface TournamentList200ResponsePayloadDataInner { * Check if a given object implements the TournamentList200ResponsePayloadDataInner interface. */ export function instanceOfTournamentList200ResponsePayloadDataInner(value: object): value is TournamentList200ResponsePayloadDataInner { + if (!('playerCount' in value) || value['playerCount'] === undefined) return false; if (!('id' in value) || value['id'] === undefined) return false; if (!('owner' in value) || value['owner'] === undefined) return false; if (!('time' in value) || value['time'] === undefined) return false; @@ -59,6 +66,7 @@ export function TournamentList200ResponsePayloadDataInnerFromJSONTyped(json: any } return { + 'playerCount': json['playerCount'], 'id': json['id'], 'owner': json['owner'], 'time': json['time'], @@ -76,6 +84,7 @@ export function TournamentList200ResponsePayloadDataInnerToJSONTyped(value?: Tou return { + 'playerCount': value['playerCount'], 'id': value['id'], 'owner': value['owner'], 'time': value['time'], diff --git a/frontend/src/pages/index.ts b/frontend/src/pages/index.ts index 4a5e931..cd6c41d 100644 --- a/frontend/src/pages/index.ts +++ b/frontend/src/pages/index.ts @@ -9,6 +9,7 @@ import './profile/profile.ts' import './logout/logout.ts' import './pongHistory/pongHistory.ts' import './tttHistory/tttHistory.ts' +import './tourHistory/tourHistory.ts' // ---- Initial load ---- setTitle(""); diff --git a/frontend/src/pages/tourHistory/tourHistory.ts b/frontend/src/pages/tourHistory/tourHistory.ts new file mode 100644 index 0000000..769d7db --- /dev/null +++ b/frontend/src/pages/tourHistory/tourHistory.ts @@ -0,0 +1,156 @@ +import { addRoute, navigateTo, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing"; +import pageAll from './tourHistoryAll.html?raw'; +import pageSingle from './tourHistorySingle.html?raw'; +import { isNullish } from "@app/utils"; +import client from "@app/api"; +import { showError, showInfo } from "@app/toast"; +import { getUser } 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 tourHistoryAll(_url: string, args: RouteHandlerParams): Promise { + + let data = await client.tournamentList(); + if (data.kind !== 'success') { + showError(`Failed to fetch data: ${data.msg}`); + return { + html: "", postInsert: async () => navigateTo('/') + } + } + return { + html: pageAll, postInsert: async (app) => { + const tourDiv = app?.querySelector('#tourList'); + if (!tourDiv || !app) + return showError('Fatal Error'); + let childrens = await Promise.all(data.payload.data.map(async (tour) => { + const ownerUser = await client.getUser({ user: tour.owner }); + const ownerUserName = ownerUser.kind === 'success' ? ownerUser.payload.name : "User Not Found"; + return { + ...tour, + ownerName: ownerUserName, + } + })) + childrens.sort((l, r) => ((new Date(r.time).getTime()) - (new Date(l.time)).getTime())); + + childrens.map(tour => { + let time = new Date(tour.time); + let owner = { id: tour.owner, name: tour.ownerName }; + let id = tour.id; + let playerCount = tour.playerCount; + + const eventItem = document.createElement('div'); + eventItem.classList.add( + 'bg-white', + 'p-4', + 'rounded-lg', + 'shadow-md', + 'mb-4', + 'flex', + 'justify-between', + 'items-center', + ); + + eventItem.innerHTML = ` +
+

Time: ${time.toDateString()} ${getHHMM(time)}

+

Created By: ${owner.name}

+

Player Count: ${playerCount}

+
+ `; + return eventItem; + }).forEach(v => tourDiv.appendChild(v)); + + } + }; +} + +async function tourHistorySingle(_url: string, args: RouteHandlerParams): Promise { + if (isNullish(args.tourid)) { + showError(`No Tournament Specified`); + return { + html: "", postInsert: async () => navigateTo('/tour') + } + } + + let data = await client.tournamentData({ id: args.tourid }); + if (data.kind !== 'success') { + showError(`Failed to fetch data: ${data.msg}`); + return { + html: "", postInsert: async () => navigateTo('/') + } + } + + let d = data.payload.data; + let user = getUser(); + if (user === null) { + showError(`You must be logged in`); + return { + html: "", postInsert: async () => navigateTo('/') + } + } + + return { + html: pageSingle, postInsert: async (app) => { + const tourOwner = app?.querySelector('#tourOwner'); + const tourTime = app?.querySelector('#tourTime'); + const tourCount = app?.querySelector('#tourCount'); + const tourPlayerList = app?.querySelector('#tourPlayerList'); + const tourGames = app?.querySelector('#tourGames'); + if (!tourOwner || !tourTime || !tourCount || !tourPlayerList || !tourGames || !app) + return showError('Fatal Error'); + + + d.users.sort((l, r) => r.score - l.score); + let time = new Date(d.time); + + const medals = ["🥇", "🥈", "🥉"]; + tourPlayerList.innerHTML = d.users.map((player, idx) => + ` + ${idx < medals.length ? `${medals[idx]}` : ''}${player.nickname} + ${player.score}`) + .join(""); + + tourOwner.innerText = await (async () => { + let req = await client.getUser({ user: d.owner }); + if (req.kind === 'success') + return req.payload.name; + return 'Unknown User'; + })(); + tourCount.innerText = d.playerCount.toString(); + tourTime.innerText = `${time.toDateString()} ${getHHMM(time)}`; + + let gameElement = d.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-slate-300'; + e.className = + 'grid grid-cols-[1fr_auto_1fr] items-center rounded-lg px-4 py-3 ' + color; + + e.innerHTML = ` +
+
${g.left.name}
+
${g.left.score}
+
+ +
${date.toDateString()}
${getHHMM(date)}
+ +
+
${g.right.name}
+
${g.right.score}
+
`; + return e; + }).filter(v => !isNullish(v)); + gameElement.forEach(e => tourGames.appendChild(e)); + } + } +} + +addRoute('/tour', tourHistoryAll); +addRoute('/tour/:tourid', tourHistorySingle); diff --git a/frontend/src/pages/tourHistory/tourHistoryAll.html b/frontend/src/pages/tourHistory/tourHistoryAll.html new file mode 100644 index 0000000..9dce3c0 --- /dev/null +++ b/frontend/src/pages/tourHistory/tourHistoryAll.html @@ -0,0 +1,9 @@ +
+
+

+ List of All Tournaments +

+
+
+
diff --git a/frontend/src/pages/tourHistory/tourHistorySingle.html b/frontend/src/pages/tourHistory/tourHistorySingle.html new file mode 100644 index 0000000..7f490e4 --- /dev/null +++ b/frontend/src/pages/tourHistory/tourHistorySingle.html @@ -0,0 +1,37 @@ +
+
+

+ Tournament +

+
+

Created By:

+

Finished At:

+

Players:

+
+
+ + + + + + + + + + + +
+ Name + + Score +
+
+
+ +
+
+
diff --git a/frontend/src/pages/ttt/ttt.css b/frontend/src/pages/ttt/ttt.css index 3162ccb..fdb4d64 100644 --- a/frontend/src/pages/ttt/ttt.css +++ b/frontend/src/pages/ttt/ttt.css @@ -5,8 +5,6 @@ src: url("/fonts/DejaVuSansMono.woff2") format("woff2"); } - - .displaybox { @apply fixed diff --git a/src/@shared/src/database/init.sql b/src/@shared/src/database/init.sql index 1a28aad..cb72332 100644 --- a/src/@shared/src/database/init.sql +++ b/src/@shared/src/database/init.sql @@ -66,6 +66,7 @@ CREATE TABLE IF NOT EXISTS tournament ( id TEXT PRIMARY KEY NOT NULL, time TEXT NOT NULL default (datetime ('now')), owner TEXT NOT NULL, + playerCount INTEGER NOT NULL, FOREIGN KEY (owner) REFERENCES user (id) ); diff --git a/src/@shared/src/database/mixin/tournament.ts b/src/@shared/src/database/mixin/tournament.ts index f4554ed..3f76b69 100644 --- a/src/@shared/src/database/mixin/tournament.ts +++ b/src/@shared/src/database/mixin/tournament.ts @@ -36,7 +36,7 @@ export const TournamentImpl: Omit = { ): TournamentData | null { // Fetch tournament const tournament = this - .prepare('SELECT id, time, owner FROM tournament WHERE id = @id') + .prepare('SELECT * FROM tournament WHERE id = @id') .get({ id }) as TournamentTable; if (!tournament) { @@ -90,7 +90,7 @@ export const TournamentImpl: Omit = { ): void { const tournamentId = newUUID() as TournamentId; - this.prepare('INSERT INTO tournament (id, owner) VALUES (@id, @owner)').run({ id: tournamentId, owner }); + this.prepare('INSERT INTO tournament (id, owner, playerCount) VALUES (@id, @owner, @count)').run({ id: tournamentId, owner, count: users.length }); for (const u of users) { this.prepare('INSERT INTO tour_user (user, nickname, score, tournament) VALUES (@id, @name, @score, @tournament)').run({ id: u.id, name: u.name, score: u.score, tournament: tournamentId }); } @@ -115,6 +115,7 @@ export interface TournamentTable { id: TournamentId; time: string; owner: UserId; + playerCount: number; } export interface TournamentUser { diff --git a/src/openapi.json b/src/openapi.json index ac15bb1..d65b4a0 100644 --- a/src/openapi.json +++ b/src/openapi.json @@ -2497,12 +2497,16 @@ "data": { "type": "object", "required": [ + "playerCount", "owner", "users", "games", "time" ], "properties": { + "playerCount": { + "type": "number" + }, "owner": { "type": "string", "description": "ownerId" @@ -2736,11 +2740,15 @@ "items": { "type": "object", "required": [ + "playerCount", "id", "owner", "time" ], "properties": { + "playerCount": { + "type": "number" + }, "id": { "type": "string", "description": "tournamentId" diff --git a/src/pong/openapi.json b/src/pong/openapi.json index 236fc30..55088f6 100644 --- a/src/pong/openapi.json +++ b/src/pong/openapi.json @@ -355,12 +355,16 @@ "data": { "type": "object", "required": [ + "playerCount", "owner", "users", "games", "time" ], "properties": { + "playerCount": { + "type": "number" + }, "owner": { "type": "string", "description": "ownerId" @@ -591,11 +595,15 @@ "items": { "type": "object", "required": [ + "playerCount", "id", "owner", "time" ], "properties": { + "playerCount": { + "type": "number" + }, "id": { "type": "string", "description": "tournamentId" diff --git a/src/pong/src/routes/tournamentData.ts b/src/pong/src/routes/tournamentData.ts index d773196..7a4b105 100644 --- a/src/pong/src/routes/tournamentData.ts +++ b/src/pong/src/routes/tournamentData.ts @@ -11,6 +11,7 @@ type TournamentDataParams = Static; const TournamentDataResponse = { '200': typeResponse('success', 'tournamentData.success', { data: Type.Object({ + playerCount: Type.Number(), owner: Type.String({ description: 'ownerId' }), users: Type.Array( Type.Object({ @@ -65,8 +66,10 @@ const route: FastifyPluginAsync = async (fastify): Promise => { 'tournamentData.failure.notFound', ); } + console.log(data); const typed_res: TournamentDataResponse['200']['payload']['data'] = { + playerCount: data.playerCount, owner: data.owner, time: data.time, users: data.users.map((v) => ({ diff --git a/src/pong/src/routes/tournamentList.ts b/src/pong/src/routes/tournamentList.ts index b779d0a..6388c6a 100644 --- a/src/pong/src/routes/tournamentList.ts +++ b/src/pong/src/routes/tournamentList.ts @@ -6,6 +6,7 @@ const TournamentListResponse = { '200': typeResponse('success', 'tournamentList.success', { data: Type.Array( Type.Object({ + playerCount: Type.Number(), id: Type.String({ description: 'tournamentId' }), owner: Type.String({ description: 'ownerId' }), time: Type.String(),