feat(tour): added tournament history page :D

This commit is contained in:
Maieul BOYER 2026-01-13 14:09:25 +01:00 committed by Maix0
parent 75f3c2a769
commit aca2dbd070
13 changed files with 245 additions and 4 deletions

View file

@ -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<any>).map(TournamentData200ResponsePayloadDataUsersInnerFromJSON)),
'games': ((json['games'] as Array<any>).map(PongHistory200ResponsePayloadDataInnerFromJSON)),
@ -99,6 +107,7 @@ export function TournamentData200ResponsePayloadDataToJSONTyped(value?: Tourname
return {
'playerCount': value['playerCount'],
'owner': value['owner'],
'users': ((value['users'] as Array<any>).map(TournamentData200ResponsePayloadDataUsersInnerToJSON)),
'games': ((value['games'] as Array<any>).map(PongHistory200ResponsePayloadDataInnerToJSON)),

View file

@ -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'],

View file

@ -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("");

View file

@ -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<RouteHandlerReturn> {
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 = `
<div class="text-left">
<p class="text-lg font-semibold text-gray-700"><strong>Time:</strong> ${time.toDateString()} ${getHHMM(time)}</p>
<p class="text-sm text-gray-500"><strong>Created By:</strong> ${owner.name} </p>
<p class="text-sm text-gray-500"><strong>Player Count:</strong> ${playerCount}</p>
</div>
<button class="bg-blue-500 text-white px-3 py-2 rounded-lg hover:bg-blue-600" onclick="navigateTo('/tour/${id}')">View</button>`;
return eventItem;
}).forEach(v => tourDiv.appendChild(v));
}
};
}
async function tourHistorySingle(_url: string, args: RouteHandlerParams): Promise<RouteHandlerReturn> {
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<HTMLSpanElement>('#tourOwner');
const tourTime = app?.querySelector<HTMLSpanElement>('#tourTime');
const tourCount = app?.querySelector<HTMLSpanElement>('#tourCount');
const tourPlayerList = app?.querySelector<HTMLTableElement>('#tourPlayerList');
const tourGames = app?.querySelector<HTMLDivElement>('#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) =>
`<tr class="${player.id === user.id ? "bg-amber-400 hover:bg-amber-500" : "hover:bg-gray-50"}" key="${player.id}">
<td class="px-4 py-2 text-sm text-gray-800 text-center border-b font-semibold min-w-100px">${idx < medals.length ? `<span class="font-lg">${medals[idx]}</span>` : ''}${player.nickname}</td>
<td class="px-4 py-2 text-sm text-gray-800 text-center border-b font-bold min-w-100px">${player.score}</td></tr>`)
.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 = `
<div class="text-right">
<div class="font-semibold ${g.outcome === 'winL' ? 'text-green-600' : 'text-red-600'}">${g.left.name}</div>
<div class="text-lg ${g.outcome === 'winL' ? 'text-green-600' : 'text-red-600'}">${g.left.score}</div>
</div>
<div class="text-center text-sm text-gray-800 px-4 whitespace-nowrap">${date.toDateString()}<br />${getHHMM(date)}</div>
<div class="text-left">
<div class="font-semibold ${g.outcome === 'winR' ? 'text-green-600' : 'text-red-600'}">${g.right.name}</div>
<div class="text-lg ${g.outcome === 'winR' ? 'text-green-600' : 'text-red-600'}">${g.right.score}</div>
</div>`;
return e;
}).filter(v => !isNullish(v));
gameElement.forEach(e => tourGames.appendChild(e));
}
}
}
addRoute('/tour', tourHistoryAll);
addRoute('/tour/:tourid', tourHistorySingle);

View file

@ -0,0 +1,9 @@
<div class="fixed inset-0 flex items-center justify-center bg-[#43536b]">
<div
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-gray-200 w-212.5 p-6 rounded-xl shadow-2xl text-center z-50">
<h2 class="text-2xl font-bold text-center mb-4 text-gray-700">
List of All Tournaments
</h2>
<div id="tourList" class="max-w-3xl mx-auto space-y-2 max-h-[50dvh] overflow-scroll"></div>
</div>
</div>

View file

@ -0,0 +1,37 @@
<div class="fixed inset-0 flex items-center justify-center bg-[#43536b]">
<div
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-gray-200 w-212.5 p-6 rounded-xl shadow-2xl text-center z-50">
<h2 class="text-2xl font-bold text-center mb-4 text-gray-700">
Tournament <span id="tour-date"> </span>
</h2>
<div id="tourInfo" class="bg-white p-4 rounded-lg shadow-md mb-4">
<p class="text-lg"> <strong class="text-gray-700">Created By:<strong> <span class="text-amber-500"
id="tourOwner"></span></p>
<p class="text-lg"> <strong class="text-gray-700">Finished At:<strong> <span class="text-amber-500"
id="tourTime"></span></p>
<p class="text-lg"> <strong class="text-gray-700">Players:<strong> <span class="text-amber-500"
id="tourCount"></span></p>
</div>
<div class="max-h-[320px] overflow-y-auto overflow-x-auto border rounded-lg mb-4">
<table class="min-w-full border-collapse table-fixed">
<thead class="sticky top-0 z-10 bg-gray-100">
<tr>
<th class="px-4 py-2 text-center text-lg font-semibold text-gray-700 border-b">
Name
</th>
<th class="px-4 py-2 text-center text-lg font-semibold text-gray-700 border-b">
Score
</th>
</tr>
</thead>
<tbody id="tourPlayerList">
<!-- rows -->
</tbody>
</table>
</div>
<div id="tourGames" class="max-w-3xl mx-auto space-y-2 max-h-[25dvh] overflow-scroll">
</div>
</div>
</div>

View file

@ -5,8 +5,6 @@
src: url("/fonts/DejaVuSansMono.woff2") format("woff2");
}
.displaybox {
@apply
fixed