Main Menu UI Overhaul (#2829)

## Description:

Overhauls the Main Menu UI, visit https://menu.openfront.dev to see
everything.

## 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-09 20:26:34 -08:00
committed by GitHub
parent 848a3a5633
commit 5e6c90d9bb
60 changed files with 7671 additions and 4546 deletions
+106 -69
View File
@@ -1,31 +1,62 @@
import { LitElement, html } from "lit";
import { html } from "lit";
import { customElement, query, state } from "lit/decorators.js";
import Countries from "resources/countries.json" with { type: "json" };
import { translateText } from "./Utils";
import { BaseModal } from "./components/BaseModal";
@customElement("flag-input-modal")
export class FlagInputModal extends LitElement {
@query("o-modal") private modalEl!: HTMLElement & {
open: () => void;
close: () => void;
};
export class FlagInputModal extends BaseModal {
@query("#flag-input-modal") private modalRef!: HTMLElement;
@state() private search = "";
@state() private isModalOpen = false;
public returnTo = "";
createRenderRoot() {
return this;
updated(changedProperties: Map<string | number | symbol, unknown>) {
super.updated(changedProperties);
}
render() {
return html`
<o-modal alwaysMaximized title=${translateText("flag_input.title")}>
<div class="flex justify-center w-full p-4">
const content = html`
<div
class="h-full flex flex-col bg-black/40 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden"
>
<div
class="flex items-center mb-4 pb-2 border-b border-white/10 gap-2 shrink-0 p-6"
>
<div class="flex items-center gap-4 flex-1">
<button
@click=${() => this.close()}
class="group flex items-center justify-center w-10 h-10 rounded-full bg-white/5 hover:bg-white/10 transition-all border border-white/10"
aria-label="${translateText("common.back")}"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-5 h-5 text-gray-400 group-hover:text-white transition-colors"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
</button>
<span
class="text-white text-xl sm:text-2xl md:text-3xl font-bold uppercase tracking-widest break-words hyphens-auto"
>
${translateText("flag_input.title")}
</span>
</div>
</div>
<div class="flex justify-center w-full px-6 pb-4 shrink-0">
<input
class="h-8 border-none border border-gray-300
rounded-xl shadow-xs text-2xl text-center focus:outline-hidden
focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-black
dark:border-gray-300/60 dark:bg-gray-700 dark:text-white"
class="h-12 w-full max-w-md border border-white/10 bg-black/40
rounded-xl shadow-inner text-xl text-center focus:outline-none
focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 text-white placeholder-white/30 transition-all"
type="text"
placeholder=${translateText("flag_input.search_flag")}
@change=${this.handleSearch}
@@ -34,41 +65,59 @@ export class FlagInputModal extends LitElement {
</div>
<div
class="flex flex-wrap justify-evenly gap-4 overflow-y-auto overflow-x-hidden h-[90%]"
class="flex-1 overflow-y-auto px-6 pb-6 scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent mr-1"
>
${this.isModalOpen
? Countries.filter(
(country) =>
!country.restricted && this.includedInSearch(country),
).map(
(country) => html`
<button
@click=${() => {
this.setFlag(country.code);
this.close();
<div class="flex flex-wrap justify-center gap-4 min-h-min">
${Countries.filter(
(country) =>
!country.restricted && this.includedInSearch(country),
).map(
(country) => html`
<button
@click=${() => {
this.setFlag(country.code);
this.close();
}}
class="group relative flex flex-col items-center gap-2 p-3 rounded-xl border border-white/5 bg-white/5 hover:bg-white/10 hover:border-white/20 transition-all cursor-pointer
w-[100px] sm:w-[120px]"
>
<img
class="w-full h-auto rounded shadow-sm group-hover:scale-105 transition-transform duration-200"
src="/flags/${country.code}.svg"
loading="lazy"
@error=${(e: Event) => {
const img = e.currentTarget as HTMLImageElement;
const fallback = "/flags/xx.svg";
if (img.src && !img.src.endsWith(fallback)) {
img.src = fallback;
}
}}
class="text-center cursor-pointer border-none bg-none opacity-70
w-[calc(100%/2-15px)] sm:w-[calc(100%/4-15px)]
md:w-[calc(100%/6-15px)] lg:w-[calc(100%/8-15px)]
xl:w-[calc(100%/10-15px)] min-w-20"
/>
<span
class="text-xs font-bold text-gray-300 group-hover:text-white text-center leading-tight w-full truncate"
>${country.name}</span
>
<img
class="country-flag w-full h-auto"
src="/flags/${country.code}.svg"
@error=${(e: Event) => {
const img = e.currentTarget as HTMLImageElement;
const fallback = "/flags/xx.svg";
if (img.src && !img.src.endsWith(fallback)) {
img.src = fallback;
}
}}
/>
<span class="country-name">${country.name}</span>
</button>
`,
)
: html``}
</button>
`,
)}
</div>
</div>
</div>
`;
if (this.inline) {
return content;
}
return html`
<o-modal
id="flag-input-modal"
title=${translateText("flag_input.title")}
?inline=${this.inline}
hideHeader
hideCloseButton
>
${content}
</o-modal>
`;
}
@@ -95,29 +144,17 @@ export class FlagInputModal extends LitElement {
);
}
public open() {
this.isModalOpen = true;
this.modalEl?.open();
}
public close() {
this.isModalOpen = false;
this.modalEl?.close();
protected onOpen(): void {
// No custom logic needed
}
connectedCallback() {
super.connectedCallback();
window.addEventListener("keydown", this.handleKeyDown);
}
disconnectedCallback() {
window.removeEventListener("keydown", this.handleKeyDown);
super.disconnectedCallback();
}
private handleKeyDown = (e: KeyboardEvent) => {
if (e.code === "Escape") {
e.preventDefault();
this.close();
protected onClose(): void {
if (this.returnTo) {
const returnEl = document.querySelector(this.returnTo) as any;
if (returnEl?.open) {
returnEl.open();
}
this.returnTo = "";
}
};
}
}