Files
OpenFrontIO/src/client/PublicLobby.ts
T
Aotumuri 9088adeb7a Translate all out-of-game UI (start screen, lobbies, etc.) (#316)
This PR adds full translation support for all out-of-game UI elements,
including the start screen, public/private lobby modals, and other
pre-game interfaces.

All static text has been externalized to en.json and ja.json for future
language support.
If you find any spots that are not yet translated (missing from the
JSON), please let me know.
Thanks a lot!

This is a follow-up to PR
[#305](https://github.com/openfrontio/OpenFrontIO/pull/305).

---------

Co-authored-by: Cldprv <dubois.cnm@tutanota.com>
Co-authored-by: jacks0n <rosty.west89@gmail.com>
2025-03-24 17:12:04 -07:00

186 lines
5.6 KiB
TypeScript

import { LitElement, html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { Difficulty, GameMapType, GameType } from "../core/game/Game";
import { consolex } from "../core/Consolex";
import { getMapsImage } from "./utilities/Maps";
import { GameID, GameInfo } from "../core/Schemas";
import { translateText } from "../client/Utils";
@customElement("public-lobby")
export class PublicLobby extends LitElement {
@state() private lobbies: GameInfo[] = [];
@state() public isLobbyHighlighted: boolean = false;
@state() private isButtonDebounced: boolean = false;
private lobbiesInterval: number | null = null;
private currLobby: GameInfo = null;
private debounceDelay: number = 750;
private lobbyIDToStart = new Map<GameID, number>();
createRenderRoot() {
return this;
}
connectedCallback() {
super.connectedCallback();
this.fetchAndUpdateLobbies();
this.lobbiesInterval = window.setInterval(
() => this.fetchAndUpdateLobbies(),
1000,
);
}
disconnectedCallback() {
super.disconnectedCallback();
if (this.lobbiesInterval !== null) {
clearInterval(this.lobbiesInterval);
this.lobbiesInterval = null;
}
}
private async fetchAndUpdateLobbies(): Promise<void> {
try {
this.lobbies = await this.fetchLobbies();
this.lobbies.forEach((l) => {
// Store the start time on first fetch because endpoint is cached, causing
// the time to appear irregular.
if (!this.lobbyIDToStart.has(l.gameID)) {
this.lobbyIDToStart.set(l.gameID, l.msUntilStart + Date.now());
}
});
} catch (error) {
consolex.error("Error fetching lobbies:", error);
}
}
async fetchLobbies(): Promise<GameInfo[]> {
try {
const response = await fetch(`/api/public_lobbies`);
if (!response.ok)
throw new Error(`HTTP error! status: ${response.status}`);
const data = await response.json();
return data.lobbies;
} catch (error) {
consolex.error("Error fetching lobbies:", error);
throw error;
}
}
public stop() {
if (this.lobbiesInterval !== null) {
this.isLobbyHighlighted = false;
clearInterval(this.lobbiesInterval);
this.lobbiesInterval = null;
}
}
render() {
if (this.lobbies.length === 0) return html``;
const lobby = this.lobbies[0];
if (!lobby?.gameConfig) {
return;
}
const timeRemaining = Math.max(
0,
Math.floor((this.lobbyIDToStart.get(lobby.gameID) - Date.now()) / 1000),
);
// Format time to show minutes and seconds
const minutes = Math.floor(timeRemaining / 60);
const seconds = timeRemaining % 60;
const timeDisplay = minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
const playersRemainingBeforeMax =
lobby.gameConfig.maxPlayers - lobby.numClients;
return html`
<button
@click=${() => this.lobbyClicked(lobby)}
?disabled=${this.isButtonDebounced}
class="w-full mx-auto p-4 md:p-6 ${this.isLobbyHighlighted
? "bg-gradient-to-r from-green-600 to-green-500"
: "bg-gradient-to-r from-blue-600 to-blue-500"} text-white font-medium rounded-xl transition-opacity duration-200 hover:opacity-90 ${this
.isButtonDebounced
? "opacity-70 cursor-not-allowed"
: ""}"
>
<div class="text-lg md:text-2xl font-semibold mb-2">
${translateText("public_lobby.join")}
</div>
<div class="flex">
<img
src="${getMapsImage(lobby.gameConfig.gameMap)}"
alt="${lobby.gameConfig.gameMap}"
class="w-1/3 md:w-1/5 md:h-[80px]"
style="border: 1px solid rgba(255, 255, 255, 0.5)"
/>
<div
class="w-full flex flex-col md:flex-row items-center justify-center gap-4"
>
<div class="flex flex-col items-start">
<div class="text-md font-medium text-blue-100">
<!-- ${lobby.gameConfig.gameMap} -->
${translateText(
`map.${lobby.gameConfig.gameMap.toLowerCase().replace(/\s+/g, "")}`,
)}
</div>
</div>
<div class="flex flex-col items-start">
<div class="text-md font-medium text-blue-100">
${lobby.numClients} / ${lobby.gameConfig.maxPlayers}
${translateText("public_lobby.waiting")}
</div>
</div>
<div class="flex items-center">
<div
class="min-w-20 text-sm font-medium px-2 py-1 bg-white/10 rounded-xl text-blue-100 text-center"
>
${timeDisplay}
</div>
</div>
</div>
</div>
</button>
`;
}
leaveLobby() {
this.isLobbyHighlighted = false;
this.currLobby = null;
}
private lobbyClicked(lobby: GameInfo) {
if (this.isButtonDebounced) {
return;
}
// Set debounce state
this.isButtonDebounced = true;
// Reset debounce after delay
setTimeout(() => {
this.isButtonDebounced = false;
}, this.debounceDelay);
if (this.currLobby == null) {
this.isLobbyHighlighted = true;
this.currLobby = lobby;
this.dispatchEvent(
new CustomEvent("join-lobby", {
detail: lobby,
bubbles: true,
composed: true,
}),
);
} else {
this.dispatchEvent(
new CustomEvent("leave-lobby", {
detail: { lobby: this.currLobby },
bubbles: true,
composed: true,
}),
);
this.leaveLobby();
}
}
}