Files
OpenFrontIO/src/client/components/RankedModal.ts
T
Ryan 70f2abb181 Homepage update & add 3 public lobbies (#3191)
## Description:

Update UI 
check https://homepageupdate.openfront.dev/ 

Improved mobile UI (now fills whole screen for all modals) e.g.:
<img width="432" height="852" alt="image"
src="https://github.com/user-attachments/assets/56de40af-4137-4c57-96b7-3910c9a665b8"
/>

Converted PublicLobby to be "GameModeSelector" to get a nicer 4x4 grid
div, where <GameModeSelector> now handles all the username validation
now (removed redundant code from modals such as matchmaking) also fixed
a bug where someone could have "[XX] X" as thier username (when the
minimum should be 3 chars for their name)

Now visually displays the 3 lobbies ffa/team/special (which is a
continuation from the work done in: #3196 )
<img width="818" height="563" alt="image"
src="https://github.com/user-attachments/assets/a15cd31b-6061-4fb8-83ee-ffde6225cfa7"
/>

updated the background:
<img width="1919" height="807" alt="image"
src="https://github.com/user-attachments/assets/358a7434-51b8-4540-baf2-d1be05053c44"
/>



slightly updated the glassy-look to be less glassy:
<img width="825" height="729" alt="image"
src="https://github.com/user-attachments/assets/1801871b-bbf8-43db-ac53-489337ae80a5"
/>



## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

w.o.n
2026-02-18 23:11:01 -06:00

180 lines
5.3 KiB
TypeScript

import { html } from "lit";
import { customElement, state } from "lit/decorators.js";
import { UserMeResponse } from "../../core/ApiSchemas";
import { getUserMe, hasLinkedAccount } from "../Api";
import { userAuth } from "../Auth";
import { translateText } from "../Utils";
import { BaseModal } from "./BaseModal";
import { modalHeader } from "./ui/ModalHeader";
@customElement("ranked-modal")
export class RankedModal extends BaseModal {
@state() private elo: number | string = "...";
@state() private userMeResponse: UserMeResponse | false = false;
@state() private errorMessage: string | null = null;
constructor() {
super();
this.id = "page-ranked";
}
connectedCallback() {
super.connectedCallback();
document.addEventListener(
"userMeResponse",
this.handleUserMeResponse as EventListener,
);
}
disconnectedCallback() {
document.removeEventListener(
"userMeResponse",
this.handleUserMeResponse as EventListener,
);
super.disconnectedCallback();
}
private handleUserMeResponse = (
event: CustomEvent<UserMeResponse | false>,
) => {
this.errorMessage = null;
this.userMeResponse = event.detail;
this.updateElo();
};
private updateElo() {
if (this.errorMessage) {
this.elo = translateText("map_component.error");
return;
}
if (hasLinkedAccount(this.userMeResponse)) {
this.elo =
this.userMeResponse &&
this.userMeResponse.player.leaderboard?.oneVone?.elo
? this.userMeResponse.player.leaderboard.oneVone.elo
: translateText("matchmaking_modal.no_elo");
}
}
protected override async onOpen(): Promise<void> {
this.elo = "...";
this.errorMessage = null;
try {
const userMe = await getUserMe();
this.userMeResponse = userMe;
} catch (error) {
console.error("Failed to fetch user profile for ranked modal", error);
this.userMeResponse = false;
this.errorMessage = translateText("map_component.error");
this.elo = translateText("map_component.error");
} finally {
this.updateElo();
}
}
createRenderRoot() {
return this;
}
render() {
const content = html`
<div class="${this.modalContainerClass}">
${modalHeader({
title: translateText("mode_selector.ranked_title"),
onBack: () => this.close(),
ariaLabel: translateText("common.back"),
})}
<div class="flex-1 min-h-0 overflow-y-auto custom-scrollbar p-6">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
${this.renderCard(
translateText("mode_selector.ranked_1v1_title"),
this.errorMessage ??
(hasLinkedAccount(this.userMeResponse)
? translateText("matchmaking_modal.elo", { elo: this.elo })
: translateText("mode_selector.ranked_title")),
() => this.handleRanked(),
)}
${this.renderDisabledCard(
translateText("mode_selector.ranked_2v2_title"),
translateText("mode_selector.coming_soon"),
)}
${this.renderDisabledCard(
translateText("mode_selector.coming_soon"),
"",
)}
${this.renderDisabledCard(
translateText("mode_selector.coming_soon"),
"",
)}
</div>
</div>
</div>
`;
if (this.inline) {
return content;
}
return html`
<o-modal ?hideHeader=${true} ?hideCloseButton=${true}>
${content}
</o-modal>
`;
}
private renderCard(title: string, subtitle: string, onClick: () => void) {
return html`
<button
@click=${onClick}
class="flex flex-col w-full h-28 sm:h-32 rounded-2xl bg-[color-mix(in_oklab,var(--frenchBlue)_70%,black)] border-0 transition-transform hover:scale-[1.02] active:scale-[0.98] p-6 items-center justify-center gap-3"
>
<div class="flex flex-col items-center gap-1 text-center">
<h3
class="text-lg sm:text-xl font-bold text-white uppercase tracking-widest leading-tight"
>
${title}
</h3>
<p
class="text-xs text-white/60 uppercase tracking-wider whitespace-pre-line leading-tight"
>
${subtitle}
</p>
</div>
</button>
`;
}
private renderDisabledCard(title: string, subtitle: string) {
return html`
<div
class="group relative isolate flex flex-col w-full h-28 sm:h-32 overflow-hidden rounded-2xl bg-slate-900/40 backdrop-blur-md border-0 shadow-none p-6 items-center justify-center gap-3 opacity-50 cursor-not-allowed"
>
<div class="flex flex-col items-center gap-1 text-center">
<h3
class="text-lg sm:text-xl font-bold text-white/60 uppercase tracking-widest leading-tight"
>
${title}
</h3>
<p
class="text-xs text-white/40 uppercase tracking-wider whitespace-pre-line leading-tight"
>
${subtitle}
</p>
</div>
</div>
`;
}
private async handleRanked() {
if ((await userAuth()) === false) {
this.close();
window.showPage?.("page-account");
return;
}
document.dispatchEvent(new CustomEvent("open-matchmaking"));
}
}