diff --git a/src/client/Layout.ts b/src/client/Layout.ts
index 4e33ab2bb..e138f9706 100644
--- a/src/client/Layout.ts
+++ b/src/client/Layout.ts
@@ -1,81 +1,84 @@
export function initLayout() {
- const hb = document.getElementById("hamburger-btn");
- const sidebar = document.getElementById("sidebar-menu");
- const backdrop = document.getElementById("mobile-menu-backdrop");
+ // Wait for play-page component to render before setting up hamburger menu
+ customElements.whenDefined("play-page").then(() => {
+ const hb = document.getElementById("hamburger-btn");
+ const sidebar = document.getElementById("sidebar-menu");
+ const backdrop = document.getElementById("mobile-menu-backdrop");
- // Force sidebar visibility style to ensure it's not hidden by other CSS
- if (sidebar && window.innerWidth < 768) {
- sidebar.style.display = "flex";
- }
-
- if (!hb) {
- console.error("Hamburger button not found");
- return;
- }
-
- // Disable fallback inline handler now that JS is loaded
- hb.onclick = null;
-
- if (!sidebar) {
- console.error("Sidebar menu not found");
- return;
- }
- if (!backdrop) {
- console.error("Mobile menu backdrop not found");
- return;
- }
-
- const setMenuState = (open: boolean) => {
- sidebar.classList.toggle("open", open);
- backdrop.classList.toggle("open", open);
- document.documentElement.classList.toggle("overflow-hidden", open);
- hb.setAttribute("aria-expanded", open ? "true" : "false");
- };
-
- const closeMenu = () => setMenuState(false);
- const openMenu = () => setMenuState(true);
-
- const toggle = (e: Event) => {
- e.stopPropagation();
- // Only prevent default if it's a touchstart to avoid ghost clicks
- if ((e as any).type === "touchstart") {
- (e as Event).preventDefault();
+ // Force sidebar visibility style to ensure it's not hidden by other CSS
+ if (sidebar && window.innerWidth < 768) {
+ sidebar.style.display = "flex";
}
- const opening = !sidebar.classList.contains("open");
- if (opening) {
- openMenu();
- } else {
- closeMenu();
+ if (!hb) {
+ console.error("Hamburger button not found");
+ return;
}
- };
- hb.addEventListener("click", toggle);
+ // Disable fallback inline handler now that JS is loaded
+ hb.onclick = null;
- backdrop.addEventListener("click", closeMenu);
-
- // Close menu when clicking a menu link or button (Mobile only)
- sidebar.addEventListener("click", (e) => {
- // On desktop, we want the menu to stay open unless explicitly toggled
- if (window.innerWidth >= 768) return;
-
- // If the click happened on or inside an anchor/button/menu item, close the menu
- const clickedElement = (e.target as Element).closest
- ? (e.target as Element).closest(
- 'a, button, [role="menuitem"], .nav-menu-item',
- )
- : null;
-
- if (clickedElement) {
- closeMenu();
+ if (!sidebar) {
+ console.error("Sidebar menu not found");
+ return;
}
- });
-
- // Close on Escape (Mobile only)
- document.addEventListener("keydown", (e) => {
- if (window.innerWidth >= 768) return;
- if (e.key === "Escape" && sidebar.classList.contains("open")) {
- closeMenu();
+ if (!backdrop) {
+ console.error("Mobile menu backdrop not found");
+ return;
}
+
+ const setMenuState = (open: boolean) => {
+ sidebar.classList.toggle("open", open);
+ backdrop.classList.toggle("open", open);
+ document.documentElement.classList.toggle("overflow-hidden", open);
+ hb.setAttribute("aria-expanded", open ? "true" : "false");
+ };
+
+ const closeMenu = () => setMenuState(false);
+ const openMenu = () => setMenuState(true);
+
+ const toggle = (e: Event) => {
+ e.stopPropagation();
+ // Only prevent default if it's a touchstart to avoid ghost clicks
+ if ((e as any).type === "touchstart") {
+ (e as Event).preventDefault();
+ }
+
+ const opening = !sidebar.classList.contains("open");
+ if (opening) {
+ openMenu();
+ } else {
+ closeMenu();
+ }
+ };
+
+ hb.addEventListener("click", toggle);
+
+ backdrop.addEventListener("click", closeMenu);
+
+ // Close menu when clicking a menu link or button (Mobile only)
+ sidebar.addEventListener("click", (e) => {
+ // On desktop, we want the menu to stay open unless explicitly toggled
+ if (window.innerWidth >= 768) return;
+
+ // If the click happened on or inside an anchor/button/menu item, close the menu
+ const clickedElement = (e.target as Element).closest
+ ? (e.target as Element).closest(
+ 'a, button, [role="menuitem"], .nav-menu-item',
+ )
+ : null;
+
+ if (clickedElement) {
+ closeMenu();
+ }
+ });
+
+ // Close on Escape (Mobile only)
+ document.addEventListener("keydown", (e) => {
+ if (window.innerWidth >= 768) return;
+ if (e.key === "Escape" && sidebar.classList.contains("open")) {
+ closeMenu();
+ }
+ });
});
}
diff --git a/src/client/Main.ts b/src/client/Main.ts
index 4d91c0880..1b4a86cb7 100644
--- a/src/client/Main.ts
+++ b/src/client/Main.ts
@@ -23,7 +23,6 @@ import { GutterAds } from "./GutterAds";
import { HelpModal } from "./HelpModal";
import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal";
import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal";
-import "./KeybindsModal";
import "./LangSelector";
import { LangSelector } from "./LangSelector";
import { initLayout } from "./Layout";
@@ -31,6 +30,7 @@ import "./Matchmaking";
import { MatchmakingModal } from "./Matchmaking";
import { initNavigation } from "./Navigation";
import "./NewsModal";
+import "./PatternInput";
import "./PublicLobby";
import { PublicLobby } from "./PublicLobby";
import { SinglePlayerModal } from "./SinglePlayerModal";
@@ -44,7 +44,17 @@ import {
import { UserSettingModal } from "./UserSettingModal";
import "./UsernameInput";
import { UsernameInput } from "./UsernameInput";
-import { incrementGamesPlayed, isInIframe } from "./Utils";
+import {
+ getDiscordAvatarUrl,
+ incrementGamesPlayed,
+ isInIframe,
+ translateText,
+} from "./Utils";
+import "./components/DesktopNavBar";
+import "./components/Footer";
+import "./components/MainLayout";
+import "./components/MobileNavBar";
+import "./components/PlayPage";
import "./components/baseComponents/Button";
import "./components/baseComponents/Modal";
import "./styles.css";
@@ -54,6 +64,100 @@ import "./styles/layout/container.css";
import "./styles/layout/header.css";
import "./styles/modal/chat.css";
+function updateAccountNavButton(userMeResponse: UserMeResponse | false) {
+ const button = document.getElementById("nav-account-button");
+ if (!button) return;
+
+ const avatarEl = document.getElementById("nav-account-avatar") as
+ | (HTMLImageElement & { _navToken?: symbol })
+ | null;
+ const personIconEl = document.getElementById(
+ "nav-account-person-icon",
+ ) as SVGElement | null;
+ const emailBadgeEl = document.getElementById(
+ "nav-account-email-badge",
+ ) as HTMLElement | null;
+ const signInTextEl = document.getElementById(
+ "nav-account-signin-text",
+ ) as HTMLSpanElement | null;
+
+ // Unique token for this update call
+ const navToken = Symbol();
+ if (avatarEl) avatarEl._navToken = navToken;
+
+ const showAvatar = (src: string, alt?: string) => {
+ if (avatarEl) {
+ avatarEl.alt = alt ?? translateText("main.discord_avatar_alt");
+ // If the avatar fails to load (bad URL / CDN issue / offline), fall back
+ // to the default sign-in UI instead of leaving a broken image.
+ avatarEl.onerror = () => {
+ // Only handle if this is the latest update
+ if (avatarEl._navToken !== navToken) return;
+ avatarEl.src = "";
+ // If the user is still logged in via email, show the email badge state.
+ const email =
+ userMeResponse !== false ? userMeResponse.user.email : undefined;
+ if (email) {
+ showEmailLoggedIn();
+ } else {
+ showSignIn();
+ }
+ };
+ avatarEl.onload = () => {
+ // Only handle if this is the latest update
+ if (avatarEl._navToken !== navToken) return;
+ // Clear error handler after a successful load.
+ avatarEl.onerror = null;
+ };
+ avatarEl.src = src;
+ avatarEl.classList.remove("hidden");
+ }
+ personIconEl?.classList.add("hidden");
+ emailBadgeEl?.classList.add("hidden");
+ signInTextEl?.classList.add("hidden");
+ button?.classList.remove("border", "border-white/20");
+ };
+
+ const showSignIn = () => {
+ avatarEl?.classList.add("hidden");
+ personIconEl?.classList.remove("hidden");
+ emailBadgeEl?.classList.add("hidden");
+ signInTextEl?.classList.remove("hidden");
+ // Restore border when showing signin state
+ button?.classList.add("border", "border-white/20");
+ };
+
+ const showEmailLoggedIn = () => {
+ avatarEl?.classList.add("hidden");
+ personIconEl?.classList.remove("hidden");
+ emailBadgeEl?.classList.remove("hidden");
+ signInTextEl?.classList.add("hidden");
+ button?.classList.add("border", "border-white/20");
+ };
+
+ const discord =
+ userMeResponse !== false ? userMeResponse.user.discord : undefined;
+ if (discord && avatarEl) {
+ const avatarAlt = translateText("main.user_avatar_alt", {
+ username: discord.username,
+ });
+ const url = getDiscordAvatarUrl(discord);
+ if (url) {
+ showAvatar(url, avatarAlt);
+ return;
+ }
+ }
+
+ const email =
+ userMeResponse !== false ? userMeResponse.user.email : undefined;
+ if (email) {
+ showEmailLoggedIn();
+ return;
+ }
+
+ showSignIn();
+}
+
declare global {
interface Window {
turnstile: any;
@@ -129,6 +233,10 @@ class Client {
// the user joins a lobby.
this.turnstileTokenPromise = getTurnstileToken();
+ // Wait for components to render before setting version
+ await customElements.whenDefined("mobile-nav-bar");
+ await customElements.whenDefined("desktop-nav-bar");
+
const versionElements = document.querySelectorAll(
"#game-version, .game-version-display",
);
@@ -233,25 +341,13 @@ class Client {
console.warn("Flag input modal element not found");
}
- // Wait for the flag-input component to be fully ready
- customElements.whenDefined("flag-input").then(() => {
- // Use a small delay to ensure the component has rendered
- setTimeout(() => {
- const flagButton = document.querySelector(
- "#flag-input-component #flag-input_",
- );
- if (!flagButton) {
- console.warn("Flag button not found inside component");
- return;
+ // Attach listener to any flag-input component (desktop or potentially others)
+ document.querySelectorAll("flag-input").forEach((flagInput) => {
+ flagInput.addEventListener("flag-input-click", () => {
+ if (flagInputModal && flagInputModal instanceof FlagInputModal) {
+ flagInputModal.open();
}
- flagButton.addEventListener("click", (e) => {
- e.preventDefault();
- e.stopPropagation();
- if (flagInputModal && flagInputModal instanceof FlagInputModal) {
- flagInputModal.open();
- }
- });
- }, 100);
+ });
});
this.patternsModal = document.getElementById(
@@ -263,49 +359,27 @@ class Client {
) {
console.warn("Territory patterns modal element not found");
}
- const patternButton = document.getElementById(
- "territory-patterns-input-preview-button",
- );
- if (isInIframe() && patternButton) {
- patternButton.style.display = "none";
- }
- // Move button to desktop wrapper on large screens
- const desktopWrapper = document.getElementById(
- "territory-patterns-preview-desktop-wrapper",
- );
- if (desktopWrapper && patternButton) {
- const moveButtonBasedOnScreenSize = () => {
- if (window.innerWidth >= 1024) {
- // Desktop: move to wrapper
- if (
- patternButton.parentElement?.id !==
- "territory-patterns-preview-desktop-wrapper"
- ) {
- patternButton.className =
- "w-full h-[60px] border border-white/20 bg-white/5 hover:bg-white/10 active:bg-white/20 rounded-lg cursor-pointer focus:outline-none transition-all duration-200 hover:scale-105 overflow-hidden";
- patternButton.style.backgroundSize = "auto 100%";
- patternButton.style.backgroundRepeat = "repeat-x";
- desktopWrapper.appendChild(patternButton);
- }
- } else {
- // Mobile: move back to bar
- const mobileParent = document.querySelector(".lg\\:col-span-9.flex");
- if (
- mobileParent &&
- patternButton.parentElement?.id ===
- "territory-patterns-preview-desktop-wrapper"
- ) {
- patternButton.className =
- "aspect-square h-[40px] sm:h-[50px] lg:hidden border border-white/20 bg-white/5 hover:bg-white/10 active:bg-white/20 rounded-lg cursor-pointer focus:outline-none transition-all duration-200 hover:scale-105 overflow-hidden shrink-0";
- patternButton.style.backgroundSize = "";
- patternButton.style.backgroundRepeat = "";
- mobileParent.appendChild(patternButton);
+ // Attach listener to any pattern-input component
+ document.querySelectorAll("pattern-input").forEach((patternInput) => {
+ patternInput.addEventListener("pattern-input-click", () => {
+ // Open the Store page which contains the patterns UI
+ window.showPage?.("page-item-store");
+ const skinStoreModal = document.getElementById(
+ "page-item-store",
+ ) as HTMLElement & { open?: (opts: any) => void };
+ if (skinStoreModal) {
+ skinStoreModal.classList.remove("hidden");
+ if (typeof skinStoreModal.open === "function") {
+ skinStoreModal.open({ showOnlyOwned: true });
}
}
- };
- moveButtonBasedOnScreenSize();
- window.addEventListener("resize", moveButtonBasedOnScreenSize);
+ });
+ });
+
+ if (isInIframe()) {
+ const mobilePat = document.getElementById("pattern-input-mobile");
+ if (mobilePat) mobilePat.style.display = "none";
}
if (
@@ -314,13 +388,17 @@ class Client {
) {
console.warn("Territory patterns modal element not found");
}
- if (patternButton === null)
- throw new Error("territory-patterns-input-preview-button");
- this.patternsModal.previewButton = patternButton;
+
+ // We no longer need to manually manage the preview button as PatternInput handles it component-side.
+ // However, we still want to ensure the modal can be opened.
+ // The setupPatternInput above handles the click event for the new buttons.
+
this.patternsModal.refresh();
- // Listen for pattern selection to update preview button
+
+ // Listen for pattern selection to update any other listeners if needed,
+ // though PatternInput handles its own updates via window event.
this.patternsModal.addEventListener("pattern-selected", () => {
- this.patternsModal.refresh();
+ // PatternInput components will update themselves.
});
window.addEventListener("showPage", (e: any) => {
@@ -331,19 +409,6 @@ class Client {
}
});
- patternButton.addEventListener("click", () => {
- window.showPage?.("page-item-store");
- const skinStoreModal = document.getElementById(
- "page-item-store",
- ) as HTMLElement & { open?: (opts: any) => void };
- if (skinStoreModal) {
- skinStoreModal.classList.remove("hidden");
- if (typeof skinStoreModal.open === "function") {
- skinStoreModal.open({ showOnlyOwned: true });
- }
- }
- });
-
this.tokenLoginModal = document.querySelector(
"token-login",
) as TokenLoginModal;
@@ -397,6 +462,12 @@ class Client {
});
}
+ if (matchmakingButtonLoggedOut) {
+ matchmakingButtonLoggedOut.addEventListener("click", () => {
+ window.showPage?.("page-account");
+ });
+ }
+
const onUserMe = async (userMeResponse: UserMeResponse | false) => {
// Check if user has actual authentication (discord or email), not just a publicId
const loggedIn =
@@ -407,6 +478,7 @@ class Client {
(userMeResponse.user.discord !== undefined ||
userMeResponse.user.email !== undefined);
updateMatchmakingButton(loggedIn);
+ updateAccountNavButton(userMeResponse);
document.dispatchEvent(
new CustomEvent("userMeResponse", {
detail: userMeResponse,
diff --git a/src/client/Matchmaking.ts b/src/client/Matchmaking.ts
index 59d1fbd0d..a0fff3cdd 100644
--- a/src/client/Matchmaking.ts
+++ b/src/client/Matchmaking.ts
@@ -48,7 +48,7 @@ export class MatchmakingModal extends BaseModal {
const content = html`
{
- // Hide all pages
- document.querySelectorAll(".page-content").forEach((el) => {
- el.classList.add("hidden");
- el.classList.remove("block");
- });
- document.getElementById("page-play")?.classList.add("hidden");
+ (window as any).currentPageId = pageId;
- const target = document.getElementById(pageId);
- if (target) {
- target.classList.remove("hidden");
- // Modals need block display explicitly
- if (target.classList.contains("page-content")) {
- target.classList.add("block");
- }
+ // Hide only the currently visible modal
+ const visibleModal = document.querySelector(".page-content:not(.hidden)");
+ if (visibleModal) {
+ visibleModal.classList.add("hidden");
+ visibleModal.classList.remove("block");
+ }
- // If the target itself is a modal component with inline attribute, open it
- if (
- target.hasAttribute("inline") &&
- typeof (target as any).open === "function"
- ) {
- (target as any).open();
+ // Handle page-play separately (it's not a page-content element)
+ const pagePlayEl = document.getElementById("page-play");
+ if (pageId === "page-play") {
+ pagePlayEl?.classList.remove("hidden");
+ } else {
+ pagePlayEl?.classList.add("hidden");
+ }
+
+ // Show the target page if it's a modal
+ if (pageId !== "page-play") {
+ const target = document.getElementById(pageId);
+ if (target) {
+ target.classList.remove("hidden");
+ // Modals need block display explicitly
+ if (target.classList.contains("page-content")) {
+ target.classList.add("block");
+ }
+
+ // If the target itself is a modal component with inline attribute, open it
+ if (
+ target.hasAttribute("inline") &&
+ typeof (target as any).open === "function"
+ ) {
+ (target as any).open();
+ }
}
}
@@ -39,39 +52,63 @@ export function initNavigation() {
window.showPage = showPage;
- document.querySelectorAll(".nav-menu-item[data-page]").forEach((el) => {
- el.addEventListener("click", () => {
- const pageId = (el as HTMLElement).dataset.page;
+ // Use event delegation for navigation items (they may be inside Lit components)
+ document.addEventListener("click", (e) => {
+ const target = (e.target as HTMLElement).closest(
+ ".nav-menu-item[data-page]",
+ );
+ if (target) {
+ const pageId = (target as HTMLElement).dataset.page;
if (pageId) showPage(pageId);
- });
+ }
});
- // Handle clicks on main container to close open modals (navigate back)
- const mainEl = document.querySelector("main");
- if (mainEl) {
- mainEl.addEventListener("click", (e: Event) => {
- const target = e.target as HTMLElement;
- const isPlayPageHidden = document
- .getElementById("page-play")
- ?.classList.contains("hidden");
+ // Wait for main-layout component to render before setting up click handler
+ customElements.whenDefined("main-layout").then(() => {
+ // Handle clicks on main container to close open modals (navigate back)
+ const mainEl = document.querySelector("main");
- // Only proceed if we are NOT on the play page (meaning a modal page is open)
- if (isPlayPageHidden) {
- // If clicking on the main container directly (e.g. padding/background)
- // or the max-width wrapper div directly
- const wrapper = mainEl.firstElementChild as HTMLElement;
- if (target === mainEl || (wrapper && target === wrapper)) {
- showPage("page-play");
+ if (mainEl) {
+ mainEl.addEventListener("click", (e: Event) => {
+ const target = e.target as HTMLElement;
+ const isPlayPageHidden = document
+ .getElementById("page-play")
+ ?.classList.contains("hidden");
+
+ // Only proceed if we are NOT on the play page (meaning a modal page is open)
+ if (isPlayPageHidden) {
+ // Close modal if clicking on main element itself, or directly on a page-content element
+ const isOnMain = target === mainEl;
+ const isOnPageContent = target.classList.contains("page-content");
+
+ if (isOnMain || isOnPageContent) {
+ // Find the open modal and call its close() method instead of showPage directly
+ // This ensures proper cleanup (like websocket disconnection)
+ const openModal = document.querySelector(
+ ".page-content:not(.hidden)",
+ ) as any;
+
+ if (openModal && typeof openModal.close === "function") {
+ // Call leaveLobby or closeAndLeave first if it exists (for lobby modals)
+ if (typeof openModal.leaveLobby === "function") {
+ openModal.leaveLobby();
+ } else if (typeof openModal.closeAndLeave === "function") {
+ openModal.closeAndLeave();
+ return; // closeAndLeave already calls close()
+ }
+ openModal.close();
+ } else {
+ showPage("page-play");
+ }
+ }
}
- }
- });
- }
+ });
+ }
+ });
- // Set default active if not set
- const initialPage = document.querySelector(
- '.nav-menu-item[data-page="page-play"]',
- );
- if (initialPage && !initialPage.classList.contains("active")) {
+ // Set default page to play if no menu item is active
+ const anyActive = document.querySelector(".nav-menu-item.active");
+ if (!anyActive) {
showPage("page-play");
}
}
diff --git a/src/client/NewsModal.ts b/src/client/NewsModal.ts
index 2cca111c7..66693e614 100644
--- a/src/client/NewsModal.ts
+++ b/src/client/NewsModal.ts
@@ -18,7 +18,7 @@ export class NewsModal extends BaseModal {
const content = html`
| null = null;
+
+function getCachedCosmetics(): Promise
{
+ if (!cosmeticsCache) {
+ const fetchPromise = fetchCosmetics();
+ cosmeticsCache = fetchPromise.catch((err) => {
+ cosmeticsCache = null;
+ throw err;
+ });
+ }
+ return cosmeticsCache;
+}
+
+@customElement("pattern-input")
+export class PatternInput extends LitElement {
+ @state() public pattern: PlayerPattern | null = null;
+ @state() public selectedColor: string | null = null;
+ @state() private isLoading: boolean = true;
+
+ @property({ type: Boolean, attribute: "show-select-label" })
+ public showSelectLabel: boolean = false;
+
+ private userSettings = new UserSettings();
+ private cosmetics: Cosmetics | null = null;
+ private _abortController: AbortController | null = null;
+
+ private _onPatternSelected = () => {
+ this.updateFromSettings();
+ };
+
+ private updateFromSettings() {
+ this.selectedColor = this.userSettings.getSelectedColor() ?? null;
+
+ if (this.cosmetics) {
+ this.pattern = this.userSettings.getSelectedPatternName(this.cosmetics);
+ } else {
+ this.pattern = null;
+ }
+ }
+
+ private onInputClick(e: Event) {
+ e.preventDefault();
+ e.stopPropagation();
+ this.dispatchEvent(
+ new CustomEvent("pattern-input-click", {
+ bubbles: true,
+ composed: true,
+ }),
+ );
+ }
+
+ async connectedCallback() {
+ super.connectedCallback();
+ this._abortController = new AbortController();
+ this.isLoading = true;
+ const cosmetics = await getCachedCosmetics();
+ if (!this.isConnected) return;
+ this.cosmetics = cosmetics;
+ this.updateFromSettings();
+ if (!this.isConnected) return;
+ this.isLoading = false;
+ window.addEventListener("pattern-selected", this._onPatternSelected, {
+ signal: this._abortController.signal,
+ });
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ if (this._abortController) {
+ this._abortController.abort();
+ this._abortController = null;
+ }
+ }
+
+ createRenderRoot() {
+ return this;
+ }
+
+ render() {
+ const isDefault = this.pattern === null && this.selectedColor === null;
+ const showSelect = this.showSelectLabel && isDefault;
+ const buttonTitle = translateText("territory_patterns.title");
+
+ // Show loading state
+ if (this.isLoading) {
+ return html`
+
+ `;
+ }
+
+ let previewContent;
+ if (this.pattern) {
+ previewContent = renderPatternPreview(this.pattern, 128, 128);
+ } else {
+ previewContent = renderPatternPreview(null, 128, 128);
+ }
+
+ return html`
+
+ `;
+ }
+}
diff --git a/src/client/PublicLobby.ts b/src/client/PublicLobby.ts
index c5ac27ffb..c7516804d 100644
--- a/src/client/PublicLobby.ts
+++ b/src/client/PublicLobby.ts
@@ -26,7 +26,7 @@ export class PublicLobby extends LitElement {
private joiningInterval: number | null = null;
private currLobby: GameInfo | null = null;
- private debounceDelay: number = 750;
+ private debounceDelay: number = 150;
private lobbyIDToStart = new Map();
private lobbySocket = new PublicLobbySocket((lobbies) =>
this.handleLobbiesUpdate(lobbies),
@@ -124,111 +124,124 @@ export class PublicLobby extends LitElement {