UI refinements (#2859)

## Description:

UI Refinements requested by @evanpelle  check https://ui.openfront.dev

## 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-01-11 22:52:03 +00:00
committed by GitHub
parent 14512e4f87
commit 3e661752af
39 changed files with 1928 additions and 1573 deletions
+150 -78
View File
@@ -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,