(feat): Queue/Dequeue button and waiting for match text

This commit is contained in:
apetitco 2026-01-09 18:01:07 +01:00 committed by Maix0
parent 6bd3a01f5f
commit 272c6f319c
8 changed files with 154 additions and 20 deletions

View file

@ -144,8 +144,8 @@ nginx-dev/nginx-selfsigned.crt nginx-dev/nginx-selfsigned.key &:
fnginx: nginx-dev/nginx nginx-dev/nginx-selfsigned.crt nginx-dev/nginx-selfsigned.key
nginx-dev/nginx -p ./nginx-dev -c nginx.conf -e /dev/null >/dev/null 2>/dev/null &
-(cd ./frontend && pnpm exec tsc --noEmit --watch --preserveWatchOutput) &
-(cd ./frontend && pnpm exec vite --clearScreen false)
-(cd ./frontend && npx pnpm exec tsc --noEmit --watch --preserveWatchOutput) &
-(cd ./frontend && npx pnpm exec vite --clearScreen false)
wait
# phony

View file

@ -3,6 +3,7 @@ apis/index.ts
index.ts
models/AllowGuestMessage200Response.ts
models/AllowGuestMessage403Response.ts
models/ApiChatBroadcastPostRequest.ts
models/ChangeDesc200Response.ts
models/ChangeDesc400Response.ts
models/ChangeDesc403Response.ts

View file

@ -17,6 +17,7 @@ import * as runtime from '../runtime';
import type {
AllowGuestMessage200Response,
AllowGuestMessage403Response,
ApiChatBroadcastPostRequest,
ChangeDesc200Response,
ChangeDesc400Response,
ChangeDesc403Response,
@ -71,6 +72,8 @@ import {
AllowGuestMessage200ResponseToJSON,
AllowGuestMessage403ResponseFromJSON,
AllowGuestMessage403ResponseToJSON,
ApiChatBroadcastPostRequestFromJSON,
ApiChatBroadcastPostRequestToJSON,
ChangeDesc200ResponseFromJSON,
ChangeDesc200ResponseToJSON,
ChangeDesc400ResponseFromJSON,
@ -169,6 +172,10 @@ import {
TttHistory404ResponseToJSON,
} from '../models/index';
export interface ApiChatBroadcastPostOperationRequest {
apiChatBroadcastPostRequest: ApiChatBroadcastPostRequest;
}
export interface ChangeDescOperationRequest {
changeDescRequest: ChangeDescRequest;
}
@ -260,6 +267,53 @@ export class OpenapiOtherApi extends runtime.BaseAPI {
return await response.value();
}
/**
*/
async apiChatBroadcastPostRaw(requestParameters: ApiChatBroadcastPostOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<void>> {
if (requestParameters['apiChatBroadcastPostRequest'] == null) {
throw new runtime.RequiredError(
'apiChatBroadcastPostRequest',
'Required parameter "apiChatBroadcastPostRequest" was null or undefined when calling apiChatBroadcastPost().'
);
}
const queryParameters: any = {};
const headerParameters: runtime.HTTPHeaders = {};
headerParameters['Content-Type'] = 'application/json';
let urlPath = `/api/chat/broadcast`;
const response = await this.request({
path: urlPath,
method: 'POST',
headers: headerParameters,
query: queryParameters,
body: ApiChatBroadcastPostRequestToJSON(requestParameters['apiChatBroadcastPostRequest']),
}, 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) {
// No body response for status 200
return new runtime.VoidApiResponse(response);
}
// 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`);
}
/**
*/
async apiChatBroadcastPost(requestParameters: ApiChatBroadcastPostOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<void> {
await this.apiChatBroadcastPostRaw(requestParameters, initOverrides);
}
/**
*/
async changeDescRaw(requestParameters: ChangeDescOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise<runtime.ApiResponse<ChangeDesc200Response | ChangeDesc400Response | ChangePassword401Response | ChangeDesc403Response>> {

View file

@ -2,6 +2,7 @@
/* eslint-disable */
export * from './AllowGuestMessage200Response';
export * from './AllowGuestMessage403Response';
export * from './ApiChatBroadcastPostRequest';
export * from './ChangeDesc200Response';
export * from './ChangeDesc400Response';
export * from './ChangeDesc403Response';

View file

@ -1,6 +1,6 @@
<div class="displaybox">
<div id="mainbox" class="mainboxDisplay">
<button id="JoinQueueBtn" class="btn-style absolute top-4 right-6">New Game</button>
<button id="JoinQueueBtn" class="btn-style absolute top-4 right-6">Join Queue</button>
<h1 class="text-3xl font-bold text-gray-800">
Tic-tac-toe Box<span id="t-username"></span>
</h1><br>
@ -9,18 +9,18 @@
X
</div>
<div class="text-left">
<div class="font-semibold text-gray-800" id="playerX-name">${playerX.name}</div>
<div class="text-lg text-gray-800" id="playerX-timer">${playerX.timer}</div>
<div class="font-semibold text-gray-800" id="playerX-name"></div>
<div class="text-lg text-gray-800" id="playerX-timer"></div>
</div>
<div class="text-center text-sm text-gray-800 px-4 whitespace-nowrap">
<div id="currentPlayer" class='text-7xl font-bold'></div>
<div id="currentPlayerTimer">${currentPlayerTimer}</div>
<div id="currentPlayerTimer">Waiting for match...</div>
</div>
<div class="text-right">
<div class="font-semibold text-gray-800" id="playerO-name">${playerO.name}</div>
<div class="text-lg text-gray-800" id="playerO-timer">${playerO.timer}</div>
<div class="font-semibold text-gray-800" id="playerO-name"></div>
<div class="text-lg text-gray-800" id="playerO-timer"></div>
</div>
<div class="text-7xl text-gray-800 mx-4 font-bold" id="playerO">
O

View file

@ -15,6 +15,12 @@ declare module 'ft_state' {
}
}
enum QueueState {
InQueue = "In Queue",
InGame = "In Game",
Idle = "Join Queue",
};
document.addEventListener("ft:pageChange", () => {
if (window.__state.tttSock !== undefined) window.__state.tttSock.close();
if (window.__state.keepAliveInterval !== undefined) clearInterval(window.__state.keepAliveInterval);
@ -48,14 +54,28 @@ async function handleTTT(): Promise<RouteHandlerReturn> {
const userXString = document.getElementById("playerX-name");
const userOString = document.getElementById("playerO-name");
if (!userXString || !userOString) {
return showError('fatal error');
}
const currentPlayerIndicator = document.getElementById("currentPlayer");
if (!currentPlayerIndicator) {
const joinQueueBtn = document.getElementById("JoinQueueBtn");
const currentPlayerTimer = document.getElementById("currentPlayerTimer")
if (!userXString || !userOString || !currentPlayerIndicator || !joinQueueBtn || !currentPlayerTimer) {
return showError('fatal error');
}
joinQueueBtn.addEventListener("click", () => {
console.log('===== JOIN QUEUE BUTTON PRESSED =====');
if (joinQueueBtn.innerText !== QueueState.Idle) {
console.log("== Entering in first if ==");
if (joinQueueBtn.innerText === QueueState.InQueue) {
console.log("== Entering in second if ==");
socket.emit("dequeue");
joinQueueBtn.innerText = QueueState.Idle;
}
return;
}
joinQueueBtn.innerText = QueueState.InQueue;
socket.emit("enqueue");
});
let curGame: CurrentGameInfo | null = null;
let curGameX: {id: string, name: string} | null = null;
let curGameO: {id: string, name: string} | null = null;
@ -64,6 +84,9 @@ async function handleTTT(): Promise<RouteHandlerReturn> {
socket.on('queueEvent', (e) => showInfo(`QueueEvent: ${e}`));
socket.on('newGame', async (gameState) => {
showInfo(`newGame: ${gameState.gameId}`)
currentPlayerTimer.innerText = "";
joinQueueBtn.innerText = QueueState.InGame;
curGame = { ...gameState, lastState: null };
let resX = await client.getUser({user: curGame.playerX});
@ -86,7 +109,8 @@ async function handleTTT(): Promise<RouteHandlerReturn> {
userOString.classList.remove('text-gray-800');
userXString.classList.remove('text-red-800');
userXString.classList.add('text-gray-800');
} else if (user.id === curGameX.id) {
}
else if (user.id === curGameX.id) {
userXString.classList.add('text-red-800');
userXString.classList.remove('text-gray-800');
userOString.classList.remove('text-red-800');
@ -95,7 +119,6 @@ async function handleTTT(): Promise<RouteHandlerReturn> {
userXString.innerText = curGameX.name;
userOString.innerText = curGameO.name;
});
socket.emit('enqueue');
const cells = app.querySelectorAll<HTMLDivElement>(".ttt-cell");
@ -131,11 +154,10 @@ async function handleTTT(): Promise<RouteHandlerReturn> {
socket.on('gameEnd', () => {
curGame = null;
socket.emit('enqueue');
showInfo('Game is finished, enqueuing directly')
currentPlayerTimer.innerText = "Waiting for match...";
joinQueueBtn.innerText = QueueState.Idle;
})
socket.on('gameBoard', (u) => {
if (curGame === null) {
return showError('Got game State, but no in a game ?');
@ -153,8 +175,6 @@ async function handleTTT(): Promise<RouteHandlerReturn> {
updateUI(u.boardState);
if (u.gameState && u.gameState !== "ongoing") {
// grid?.classList.add("pointer-events-none");
if (u.gameState !== curGame.lastState) {
curGame.lastState = u.gameState;
switch (u.gameState) {

View file

@ -7,7 +7,35 @@
"components": {
"schemas": {}
},
"paths": {},
"paths": {
"/api/chat/broadcast": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"nextGame"
],
"properties": {
"nextGame": {
"type": "string"
}
}
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Default Response"
}
}
}
}
},
"servers": [
{
"url": "https://local.maix.me:8888",

View file

@ -1917,6 +1917,36 @@
]
}
},
"/api/chat/broadcast": {
"post": {
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"nextGame"
],
"properties": {
"nextGame": {
"type": "string"
}
}
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Default Response"
}
},
"tags": [
"openapi_other"
]
}
},
"/api/ttt/history/{user}": {
"get": {
"operationId": "tttHistory",