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
This commit is contained in:
Ryan
2026-02-19 05:11:01 +00:00
committed by GitHub
parent 2a7db43db3
commit 70f2abb181
48 changed files with 1016 additions and 725 deletions
+50 -1
View File
@@ -1,4 +1,4 @@
import { LitElement } from "lit";
import { html, LitElement, TemplateResult } from "lit";
import { property, query, state } from "lit/decorators.js";
/**
@@ -10,11 +10,21 @@ import { property, query, state } from "lit/decorators.js";
* - Automatic listener lifecycle management
* - Common inline/modal element handling
* - Shared open/close logic with hooks for custom behavior
* - Standardized loading spinner UI
* - Consistent modal container styling
*/
export abstract class BaseModal extends LitElement {
@state() protected isModalOpen = false;
@property({ type: Boolean }) inline = false;
/**
* Standard modal container class string.
* Provides consistent dark glassmorphic styling across all modals.
* No rounding on mobile for full-screen appearance.
*/
protected readonly modalContainerClass =
"h-full flex flex-col overflow-hidden bg-black/70 backdrop-blur-xl lg:rounded-2xl lg:border border-white/10";
@query("o-modal") protected modalEl?: HTMLElement & {
open: () => void;
close: () => void;
@@ -121,4 +131,43 @@ export abstract class BaseModal extends LitElement {
this.modalEl?.close();
}
}
/**
* Renders a standardized loading spinner with optional custom message.
* Use this for consistent loading states across all modals.
*
* @param message - Optional loading message text. Defaults to no message.
* @param spinnerColor - Optional spinner color. Defaults to 'blue'.
* @returns TemplateResult of the loading UI
*/
protected renderLoadingSpinner(
message?: string,
spinnerColor: "blue" | "green" | "yellow" | "white" = "blue",
): TemplateResult {
const colorClasses = {
blue: "border-blue-500/30 border-t-blue-500",
green: "border-green-500/30 border-t-green-500",
yellow: "border-yellow-500/30 border-t-yellow-500",
white: "border-white/20 border-t-white",
};
return html`
<div
class="flex flex-col items-center justify-center p-12 text-white h-full min-h-[400px]"
>
<div
class="w-12 h-12 border-4 ${colorClasses[
spinnerColor
]} rounded-full animate-spin mb-4"
></div>
${message
? html`<p
class="text-white/60 font-medium tracking-wide animate-pulse"
>
${message}
</p>`
: ""}
</div>
`;
}
}
+16 -7
View File
@@ -19,7 +19,7 @@ export class DesktopNavBar extends LitElement {
super.connectedCallback();
window.addEventListener("showPage", this._onShowPage);
const current = (window as any).currentPageId;
const current = window.currentPageId;
if (current) {
// Wait for render
this.updateComplete.then(() => {
@@ -79,9 +79,12 @@ export class DesktopNavBar extends LitElement {
};
render() {
window.currentPageId ??= "page-play";
const currentPage = window.currentPageId;
return html`
<nav
class="hidden lg:flex w-full bg-slate-950/70 backdrop-blur-md border-b border-white/10 items-center justify-center gap-8 py-4 shrink-0 transition-opacity z-50 relative"
class="hidden lg:flex w-full bg-slate-900 items-center justify-center gap-8 py-4 shrink-0 z-50 relative"
>
<div class="flex flex-col items-center justify-center">
<div class="h-8 text-[#2563eb]">
@@ -89,7 +92,7 @@ export class DesktopNavBar extends LitElement {
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1364 259"
fill="currentColor"
class="h-full w-auto drop-shadow-[0_0_10px_rgba(37,99,235,0.4)]"
class="h-full w-auto"
>
<path
d="M0,174V51h15.24v-17.14h16.81v-16.98h16.96V0h1266v17.23h17.13v16.81h16.98v16.96h14.88v123h-15.13v17.08h-17.08v17.08h-16.9v17.04H324.9v16.86h-16.9v16.95h-102v-17.12h-17.07v-17.05H48.73v-17.05h-16.89v-16.89H14.94v-16.89H0ZM1297.95,17.35H65.9v16.7h-17.08v17.08h-14.5v123.08h14.85v16.9h17.08v17.08h139.9v17.08h17.08v16.36h67.9v-16.72h17.08v-17.07h989.88v-17.07h17.08v-16.9h14.44V50.8h-14.75v-17.08h-16.9v-16.37Z"
@@ -131,20 +134,26 @@ export class DesktopNavBar extends LitElement {
class="l-header__highlightText text-center"
></div>
</div>
<!-- Desktop Navigation Menu Items -->
<button
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
class="nav-menu-item ${currentPage === "page-play"
? "active"
: ""} text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
data-page="page-play"
data-i18n="main.play"
></button>
<!-- Desktop Navigation Menu Items -->
<button
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
class="nav-menu-item ${currentPage === "page-news"
? "active"
: ""} text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
data-page="page-news"
data-i18n="main.news"
></button>
<div class="relative no-crazygames">
<button
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
class="nav-menu-item ${currentPage === "page-item-store"
? "active"
: ""} text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
data-page="page-item-store"
data-i18n="main.store"
@click=${this.onStoreClick}
+2 -2
View File
@@ -10,7 +10,7 @@ export class Footer extends LitElement {
render() {
return html`
<footer
class="[.in-game_&]:hidden bg-black/60 backdrop-blur-md flex flex-col items-center justify-center gap-2 pt-[2px] pb-2 text-white/50 w-full border-t border-white/10 shrink-0 mt-auto"
class="[.in-game_&]:hidden bg-slate-950/70 backdrop-blur-md flex flex-col items-center justify-center gap-2 pt-[2px] pb-2 text-white/50 w-full border-t border-white/10 shrink-0 mt-auto"
>
<div class="flex items-center justify-center gap-6 pt-2">
<a
@@ -43,7 +43,7 @@ export class Footer extends LitElement {
</svg>
</a>
<a
href="https://discord.gg/jRpxXvG42t"
href="https://discord.gg/openfront"
target="_blank"
rel="noopener noreferrer"
class="opacity-60 hover:opacity-100 hover:scale-110 transition-all"
+1 -1
View File
@@ -45,7 +45,7 @@ export class LobbyTeamView extends LitElement {
willUpdate(changedProperties: Map<string, any>) {
// Recompute team preview when relevant properties change
// clients is 'changed' every 1s from pollPlayers, chose to not compare for actual change
// clients is updated from WebSocket lobby_info events
if (
changedProperties.has("gameMode") ||
changedProperties.has("clients") ||
+2 -2
View File
@@ -19,10 +19,10 @@ export class MainLayout extends LitElement {
render() {
return html`
<main
class="relative [.in-game_&]:hidden flex flex-col flex-1 overflow-hidden w-full px-[clamp(1.5rem,3vw,3rem)] pt-[clamp(0.75rem,1.5vw,1.5rem)] pb-[clamp(0.75rem,1.5vw,1.5rem)]"
class="relative [.in-game_&]:hidden flex flex-col flex-1 overflow-hidden w-full px-0 lg:px-[clamp(1.5rem,3vw,3rem)] pt-0 lg:pt-[clamp(0.75rem,1.5vw,1.5rem)] pb-0 lg:pb-[clamp(0.75rem,1.5vw,1.5rem)]"
>
<div
class="w-full max-w-[20cm] mx-auto flex flex-col flex-1 gap-[clamp(1.5rem,3vw,3rem)] overflow-y-auto overflow-x-hidden [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden lg:[scrollbar-width:auto] lg:[-ms-overflow-style:auto] lg:[&::-webkit-scrollbar]:block"
class="w-full lg:max-w-[20cm] mx-auto flex flex-col flex-1 gap-0 lg:gap-[clamp(1.5rem,3vw,3rem)] overflow-y-auto overflow-x-hidden"
>
${this._initialChildren}
</div>
+9 -3
View File
@@ -11,7 +11,7 @@ export class MobileNavBar extends LitElement {
super.connectedCallback();
window.addEventListener("showPage", this._onShowPage);
const current = (window as any).currentPageId;
const current = window.currentPageId;
if (current) {
this.updateComplete.then(() => {
this._updateActiveState(current);
@@ -40,6 +40,9 @@ export class MobileNavBar extends LitElement {
}
render() {
window.currentPageId ??= "page-play";
const currentPage = window.currentPageId;
return html`
<!-- Border Segments (Custom right border with gap for button) -->
<div
@@ -52,7 +55,7 @@ export class MobileNavBar extends LitElement {
></div>
<div
class="flex-1 w-full flex flex-col justify-start overflow-y-auto md:pt-[clamp(1rem,3vh,4rem)] md:pb-[clamp(0.5rem,2vh,2rem)] md:px-[clamp(1rem,1.5vw,2rem)] p-5 gap-[clamp(1rem,3vh,3rem)]"
class="flex-1 w-full flex flex-col justify-start overflow-y-auto lg:pt-[clamp(1rem,3vh,4rem)] lg:pb-[clamp(0.5rem,2vh,2rem)] lg:px-[clamp(1rem,1.5vw,2rem)] p-5 gap-[clamp(1rem,3vh,3rem)]"
>
<!-- Logo + Menu -->
<div
@@ -110,7 +113,10 @@ export class MobileNavBar extends LitElement {
</div>
<!-- Mobile Navigation Menu Items -->
<button
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] ${currentPage ===
"page-play"
? "active"
: ""}"
data-page="page-play"
data-i18n="main.play"
></button>
+106 -111
View File
@@ -11,136 +11,131 @@ export class PlayPage extends LitElement {
return html`
<div
id="page-play"
class="flex flex-col gap-2 w-full max-w-6xl mx-auto px-0 sm:px-4 transition-all duration-300 my-auto min-h-0"
class="flex flex-col gap-2 w-full lg:max-w-6xl mx-auto px-0 lg:px-4 lg:my-auto min-h-0"
>
<token-login class="absolute"></token-login>
<!-- Header / Identity Section -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-2 lg:gap-6 w-full">
<!-- Mobile: Fixed top bar -->
<div
class="lg:hidden fixed left-0 right-0 top-0 z-40 pt-[env(safe-area-inset-top)] bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)] border-b border-white/10"
>
<div
class="lg:col-span-9 flex flex-row flex-nowrap gap-x-2 h-[60px] items-center bg-slate-900/80 backdrop-blur-md p-3 rounded-xl relative z-20 text-sm sm:text-base shrink-0"
class="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center h-14 px-2 gap-2"
>
<!-- Flag -->
<div
class="h-[40px] sm:h-[50px] shrink-0 aspect-[4/3] flex items-center justify-center lg:hidden"
<button
id="hamburger-btn"
class="col-start-1 justify-self-start h-10 shrink-0 aspect-[4/3] flex text-white/90 rounded-md items-center justify-center transition-colors"
data-i18n-aria-label="main.menu"
aria-expanded="false"
aria-controls="sidebar-menu"
aria-haspopup="dialog"
data-i18n-title="main.menu"
>
<!-- Hamburger (Mobile) -->
<button
id="hamburger-btn"
class="lg:hidden flex w-full h-full bg-slate-800/40 text-white/90 hover:bg-slate-700/40 p-0 rounded-md items-center justify-center cursor-pointer transition-all duration-200"
data-i18n-aria-label="main.menu"
aria-expanded="false"
aria-controls="sidebar-menu"
aria-haspopup="dialog"
data-i18n-title="main.menu"
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-8"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-8 h-8"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
</button>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
/>
</svg>
</button>
<div
class="col-start-2 flex items-center justify-center text-[#2563eb] min-w-0"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1364 259"
fill="currentColor"
class="h-6 w-auto drop-shadow-[0_0_10px_rgba(37,99,235,0.3)] shrink-0"
>
<path
d="M0,174V51h15.24v-17.14h16.81v-16.98h16.96V0h1266v17.23h17.13v16.81h16.98v16.96h14.88v123h-15.13v17.08h-17.08v17.08h-16.9v17.04H324.9v16.86h-16.9v16.95h-102v-17.12h-17.07v-17.05H48.73v-17.05h-16.89v-16.89H14.94v-16.89H0ZM1297.95,17.35H65.9v16.7h-17.08v17.08h-14.5v123.08h14.85v16.9h17.08v17.08h139.9v17.08h17.08v16.36h67.9v-16.72h17.08v-17.07h989.88v-17.07h17.08v-16.9h14.44V50.8h-14.75v-17.08h-16.9v-16.37Z"
/>
<path
d="M189.1,154.78v17.07h-16.9v16.75h-51.07v-16.42h-16.9v-17.07h-16.97v-84.88h16.63v-17.07h16.9v-16.84h51.07v16.5h17.07v17.07h16.7v84.89h-16.54ZM137.87,53.1v17.15h-16.6v84.86h16.97v16.61h16.89v-16.97h16.6v-84.86h-16.97v-16.79h-16.89Z"
/>
<path
d="M273.91,104.06v-16.73h50.92v16.45h16.85v68.05h-16.44v17.06h-50.96v16.88h16.4v16.96h-67.28v-16.61h16.33v-101.86h-16.38v-16.98h33.4v16.63c6.12,0,11.72,0,17.31,0,0,22.56,0,45.13,0,67.75h33.59v-67.61h-33.73Z"
/>
<path
d="M631.12,188.64v-16.36h16.53V53.2h-16.25v-16.86h118.33v33.29h-16.65v-16.36h-50.72v50.44h33.36v-16.35h16.99v50.25h-16.6v-16.33h-33.73v50.65h16.37v16.72h-67.63Z"
/>
<path
d="M596.78,103.8v84.94h-33.54v-84.39h-34.03v84.25h-33.85v-101.29h84.5v16.49h16.93Z"
/>
<path
d="M1107.12,188.71v-84.34h-34.03v84.37h-33.7v-101.41h84.42v16.41h16.86v84.96h-33.54Z"
/>
<path
d="M988.1,171.78v16.87h-67.88v-16.38h-16.87v-68.06h16.38v-16.87h68.06v16.38h16.87v68.06h-16.55ZM970.78,104.35h-33.39v67.38h33.39v-67.38Z"
/>
<path
d="M460.77,155.38v16.49h-16.58v16.83h-68.05v-16.5h-16.83v-68.05h16.49v-16.83h68.05v16.49h16.83v34.06h-67.31v33.82h33.47v-16.31h33.92ZM393.39,104.18v16.56h33.3v-16.56h-33.3Z"
/>
<path
d="M1209.13,172h-16.9v-67.9h-16.96v-16.9h16.68v-17.08h16.9v-16.82h16.9v33.58h50.98v16.91h-50.4v67.96h16.48v-16.43h50.95v16.54h-16.55v16.76h-68.08v-16.6Z"
/>
<path
d="M834.91,120.94v16.96h-16.65v33.88h16.41v16.96h-67.29v-16.63h16.34v-67.87h-16.4v-16.97h50.42v33.81h17.3l-.14-.14Z"
/>
<path
d="M835.05,121.08v-33.75h33.76v16.43h16.85v33.96h-33.43v-16.79c-6.13,0-11.73,0-17.32,0,0,0,.14.14.14.14Z"
/>
</svg>
</div>
<!-- Username -->
<div class="flex-1 min-w-0 h-[40px] sm:h-[50px] flex items-center">
<div
aria-hidden="true"
class="col-start-3 justify-self-end h-10 shrink-0 aspect-[4/3]"
></div>
</div>
</div>
<div
class="w-full pb-4 lg:pb-0 flex flex-col gap-0 lg:grid lg:grid-cols-12 lg:gap-2"
>
<!-- Mobile: spacer for fixed top bar -->
<div class="lg:hidden h-[calc(env(safe-area-inset-top)+56px)]"></div>
<div
class="px-2 py-2 bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)] border-y border-white/10 overflow-visible lg:col-span-9 lg:flex lg:items-center lg:gap-x-2 lg:h-[60px] lg:p-3 lg:relative lg:z-20 lg:border-y-0 lg:rounded-xl"
>
<div class="flex items-center gap-2 min-w-0 w-full">
<username-input
class="relative w-full h-full block text-ellipsis overflow-hidden whitespace-nowrap"
class="flex-1 min-w-0 h-10 lg:h-[50px]"
></username-input>
</div>
<!-- Pattern button (Mobile - inside bar, Desktop - hidden here) -->
<pattern-input
id="pattern-input-mobile"
show-select-label
class="aspect-square h-[50px] sm:h-[50px] lg:hidden shrink-0"
></pattern-input>
</div>
<!-- Pattern & Flag buttons (Desktop only - separate column) -->
<div class="hidden lg:flex lg:col-span-3">
<div class="w-full h-[60px] flex gap-2">
<pattern-input
id="pattern-input-desktop"
id="pattern-input-mobile"
show-select-label
class="flex-1 h-full"
adaptive-size
class="shrink-0 lg:hidden"
></pattern-input>
<flag-input
id="flag-input-desktop"
show-select-label
class="flex-1 h-full"
></flag-input>
</div>
</div>
</div>
<!-- Primary Game Actions Area -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 w-full">
<!-- Left Column: Featured Lobbies / Quick Play -->
<div class="lg:col-span-9 flex flex-col gap-6 min-w-0">
<!-- Public Lobby Card -->
<public-lobby
class="block w-full transition-all duration-[50ms]"
></public-lobby>
</div>
<!-- Right Column: Custom Games & Modes -->
<div class="lg:col-span-3">
<div
class="group relative isolate flex flex-col w-full h-40 lg:h-96 overflow-hidden rounded-2xl transition-all duration-300"
>
<div
class="h-full flex flex-col bg-slate-900/40 backdrop-blur-sm rounded-2xl overflow-hidden"
>
<div
class="py-2 bg-blue-900/20 text-center text-sm font-bold text-gray-300 uppercase tracking-widest"
data-i18n="host_modal.label"
></div>
<div class="flex-1 p-2 flex flex-row lg:flex-col gap-2">
<o-button
id="single-player"
data-i18n-title="main.solo"
translationKey="main.solo"
fill
class="flex-1 transition-transform"
></o-button>
<o-button
id="host-lobby-button"
data-i18n-title="main.create"
translationKey="main.create"
fill
secondary
class="flex-1 opacity-90 hover:opacity-100"
></o-button>
<o-button
id="join-private-lobby-button"
data-i18n-title="main.join"
translationKey="main.join"
fill
secondary
class="flex-1 opacity-90 hover:opacity-100"
></o-button>
</div>
</div>
</div>
</div>
<!-- Matchmaking Buttons (Full Width across entire grid) -->
<div class="lg:col-span-12 flex flex-col gap-6">
<matchmaking-button></matchmaking-button>
<div class="hidden lg:flex lg:col-span-3 h-[60px] gap-2">
<pattern-input
id="pattern-input-desktop"
show-select-label
class="flex-1 h-full"
></pattern-input>
<flag-input
id="flag-input-desktop"
show-select-label
class="flex-1 h-full"
></flag-input>
</div>
</div>
<game-mode-selector></game-mode-selector>
</div>
`;
}
+179
View File
@@ -0,0 +1,179 @@
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"));
}
}
@@ -67,7 +67,7 @@ export class OModal extends LitElement {
const wrapperClass = this.inline
? "relative flex flex-col w-full h-full m-0 max-w-full max-h-none shadow-none"
: `relative flex flex-col w-[90%] min-w-[400px] max-w-[900px] m-8 rounded-lg shadow-[0_20px_60px_rgba(0,0,0,0.8)] max-h-[calc(100vh-4rem)] ${
: `relative flex flex-col w-full h-full lg:w-[90%] lg:h-auto lg:min-w-[400px] lg:max-w-[900px] lg:m-8 lg:rounded-lg shadow-[0_20px_60px_rgba(0,0,0,0.8)] lg:max-h-[calc(100vh-4rem)] ${
this.alwaysMaximized ? "h-auto" : ""
}`;
const wrapperStyle =
@@ -101,7 +101,7 @@ export class OModal extends LitElement {
</div>`
: html``}
<section
class="relative flex-1 min-h-0 p-[1.4rem] text-white bg-[#23232382] backdrop-blur-md rounded-lg overflow-y-auto"
class="relative flex-1 min-h-0 p-0 lg:p-[1.4rem] text-white bg-[#23232382] backdrop-blur-md lg:rounded-lg overflow-y-auto"
>
<slot></slot>
</section>
@@ -48,9 +48,9 @@ export class SettingSlider extends LitElement {
return html`
<div
class="flex flex-row items-center justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-4 ${rainbowClass}"
class="flex flex-col sm:flex-row sm:items-center sm:justify-between w-full p-4 bg-white/5 border border-white/10 rounded-xl hover:bg-white/10 transition-all gap-3 sm:gap-4 ${rainbowClass}"
>
<div class="flex flex-col flex-1 min-w-0 mr-4">
<div class="flex flex-col flex-1 min-w-0 sm:mr-4">
<label class="text-white font-bold text-base block mb-1"
>${this.label}</label
>
@@ -59,21 +59,28 @@ export class SettingSlider extends LitElement {
</div>
</div>
<div class="flex flex-col items-end gap-2 shrink-0 w-[200px]">
<span class="text-white font-bold text-sm">${this.value}%</span>
<input
type="range"
class="w-full appearance-none h-2 bg-transparent rounded outline-none
<div
class="flex flex-col items-start sm:items-end gap-2 shrink-0 w-full sm:w-[200px]"
>
<div class="flex items-center gap-2 w-full">
<input
type="range"
class="flex-1 w-auto appearance-none h-2 bg-transparent rounded outline-none
[&::-webkit-slider-runnable-track]:h-2 [&::-webkit-slider-runnable-track]:rounded [&::-webkit-slider-runnable-track]:bg-[image:linear-gradient(to_right,#3b82f6_0%,#3b82f6_var(--fill),rgba(255,255,255,0.1)_var(--fill),rgba(255,255,255,0.1)_100%)]
[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:h-[18px] [&::-webkit-slider-thumb]:w-[18px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:bg-blue-500 [&::-webkit-slider-thumb]:cursor-pointer [&::-webkit-slider-thumb]:-mt-[6px] [&::-webkit-slider-thumb]:shadow-[0_0_0_4px_rgba(59,130,246,0.2)] [&::-webkit-slider-thumb]:transition-all active:[&::-webkit-slider-thumb]:scale-110 active:[&::-webkit-slider-thumb]:shadow-[0_0_0_6px_rgba(59,130,246,0.3)]
[&::-moz-range-track]:h-2 [&::-moz-range-track]:rounded [&::-moz-range-track]:bg-white/10
[&::-moz-range-progress]:h-2 [&::-moz-range-progress]:rounded [&::-moz-range-progress]:bg-blue-500
[&::-moz-range-thumb]:h-[18px] [&::-moz-range-thumb]:w-[18px] [&::-moz-range-thumb]:border-none [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:bg-blue-500 [&::-moz-range-thumb]:cursor-pointer [&::-moz-range-thumb]:shadow-[0_0_0_4px_rgba(59,130,246,0.2)] [&::-moz-range-thumb]:transition-all active:[&::-moz-range-thumb]:scale-110 active:[&::-moz-range-thumb]:shadow-[0_0_0_6px_rgba(59,130,246,0.3)]"
min=${this.min}
max=${this.max}
.value=${String(this.value)}
@input=${this.handleInput}
/>
min=${this.min}
max=${this.max}
.value=${String(this.value)}
@input=${this.handleInput}
/>
<span
class="text-white font-bold text-sm shrink-0 text-right min-w-[3ch]"
>${this.value}%</span
>
</div>
</div>
</div>
`;
@@ -72,8 +72,8 @@ export class GameList extends LitElement {
>
${translateText("game_list.mode")}:
${game.mode === GameMode.FFA
? translateText("game_list.mode_ffa")
: html`${translateText("game_list.mode_team")}`}
? translateText("game_mode.ffa")
: html`${translateText("game_mode.teams")}`}
</div>
</div>
</div>
@@ -50,8 +50,8 @@ export class PlayerStatsTreeView extends LitElement {
private labelForMode(m: GameMode) {
return m === GameMode.FFA
? translateText("player_stats_tree.mode_ffa")
: translateText("player_stats_tree.mode_team");
? translateText("game_mode.ffa")
: translateText("game_mode.teams");
}
createRenderRoot() {
+1 -1
View File
@@ -50,7 +50,7 @@ export class MapDisplay extends LitElement {
this.isLoading = true;
const mapValue = GameMapType[this.mapKey as keyof typeof GameMapType];
const data = terrainMapFileLoader.getMapData(mapValue);
this.mapWebpPath = await data.webpPath();
this.mapWebpPath = data.webpPath;
const manifest = await data.manifest();
this.mapName = manifest.name;
this.hasNations =
+2 -2
View File
@@ -15,13 +15,13 @@ export interface ModalHeaderProps {
const DEFAULT_WRAPPER_CLASS = "flex flex-wrap items-center gap-2 shrink-0";
const DEFAULT_DIVIDER_CLASS = "border-b border-white/10";
const DEFAULT_PADDING_CLASS = "p-6";
const DEFAULT_PADDING_CLASS = "p-4 lg:p-6";
const DEFAULT_LEFT_CLASS = "flex items-center gap-4 flex-1";
const DEFAULT_BUTTON_CLASS =
"group flex items-center justify-center w-10 h-10 rounded-full shrink-0 " +
"bg-white/5 hover:bg-white/10 transition-all border border-white/10";
const DEFAULT_TITLE_CLASS =
"text-white text-xl sm:text-2xl md:text-3xl font-bold uppercase " +
"text-white text-xl lg:text-2xl font-bold uppercase " +
"tracking-widest break-words hyphens-auto";
const withClasses = (...classes: Array<string | undefined>) =>