mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-14 17:36:47 +00:00
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:
+106
-69
@@ -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 = "";
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user