mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 15:00:43 +00:00
Addition of FlagInputModal (#1606)
## Description: The UI for selecting flags differs from other existing UIs, so this change allows it to use . The actual contents for flag selection will be included in the next PR. ## 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 - [x] I have read and accepted the CLA agreement (only required once). ## Please put your Discord username so you can be contacted if a bug or regression is found: aotumuri --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
This commit is contained in:
+32
-90
@@ -1,13 +1,11 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import Countries from "./data/countries.json";
|
||||
import { renderPlayerFlag } from "../core/CustomFlag";
|
||||
const flagKey: string = "flag";
|
||||
|
||||
@customElement("flag-input")
|
||||
export class FlagInput extends LitElement {
|
||||
@state() private flag: string = "";
|
||||
@state() private search: string = "";
|
||||
@state() private showModal: boolean = false;
|
||||
@state() public flag: string = "";
|
||||
|
||||
static styles = css`
|
||||
@media (max-width: 768px) {
|
||||
@@ -21,19 +19,6 @@ export class FlagInput extends LitElement {
|
||||
}
|
||||
`;
|
||||
|
||||
private handleSearch(e: Event) {
|
||||
this.search = String((e.target as HTMLInputElement).value);
|
||||
}
|
||||
|
||||
private setFlag(flag: string) {
|
||||
if (flag === "xx") {
|
||||
flag = "";
|
||||
}
|
||||
this.flag = flag;
|
||||
this.showModal = false;
|
||||
this.storeFlag(flag);
|
||||
}
|
||||
|
||||
public getCurrentFlag(): string {
|
||||
return this.flag;
|
||||
}
|
||||
@@ -46,14 +31,6 @@ export class FlagInput extends LitElement {
|
||||
return "";
|
||||
}
|
||||
|
||||
private storeFlag(flag: string) {
|
||||
if (flag) {
|
||||
localStorage.setItem(flagKey, flag);
|
||||
} else if (flag === "") {
|
||||
localStorage.removeItem(flagKey);
|
||||
}
|
||||
}
|
||||
|
||||
private dispatchFlagEvent() {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("flag-change", {
|
||||
@@ -68,86 +45,51 @@ export class FlagInput extends LitElement {
|
||||
super.connectedCallback();
|
||||
this.flag = this.getStoredFlag();
|
||||
this.dispatchFlagEvent();
|
||||
window.addEventListener("keydown", this.handleKeyDown);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleKeyDown);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.code === "Escape") {
|
||||
e.preventDefault();
|
||||
this.showModal = false;
|
||||
}
|
||||
};
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
class="absolute left-0 top-0 w-full h-full ${this.showModal
|
||||
? ""
|
||||
: "hidden"}"
|
||||
@click=${() => (this.showModal = false)}
|
||||
></div>
|
||||
<div class="flex relative">
|
||||
<button
|
||||
@click=${() => (this.showModal = !this.showModal)}
|
||||
id="flag-input_"
|
||||
class="border p-[4px] rounded-lg flex cursor-pointer border-black/30 dark:border-gray-300/60 bg-white/70 dark:bg-[rgba(55,65,81,0.7)]"
|
||||
title="Pick a flag!"
|
||||
>
|
||||
<img class="size-[48px]" src="/flags/${this.flag || "xx"}.svg" />
|
||||
<span
|
||||
id="flag-preview"
|
||||
style="display:inline-block;width:48px;height:64px;vertical-align:middle;background:#333;border-radius:6px;overflow:hidden;"
|
||||
></span>
|
||||
</button>
|
||||
${this.showModal
|
||||
? html`
|
||||
<div
|
||||
class="text-white flex flex-col gap-[0.5rem] absolute top-[60px] left-[0px] w-[780%] h-[500px] max-h-[50vh] max-w-[87vw] bg-gray-900/80 backdrop-blur-md p-[10px] rounded-[8px] z-[3] ${this
|
||||
.showModal
|
||||
? ""
|
||||
: "hidden"}"
|
||||
>
|
||||
<input
|
||||
class="h-[2rem] border-none text-center border border-gray-300 rounded-xl shadow-sm text-2xl text-center focus:outline-none 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"
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
@change=${this.handleSearch}
|
||||
@keyup=${this.handleSearch}
|
||||
/>
|
||||
<div
|
||||
class="flex flex-wrap justify-evenly gap-[1rem] overflow-y-auto overflow-x-hidden"
|
||||
>
|
||||
${Countries.filter(
|
||||
(country) =>
|
||||
country.name
|
||||
.toLowerCase()
|
||||
.includes(this.search.toLowerCase()) ||
|
||||
country.code
|
||||
.toLowerCase()
|
||||
.includes(this.search.toLowerCase()),
|
||||
).map(
|
||||
(country) => html`
|
||||
<button
|
||||
@click=${() => this.setFlag(country.code)}
|
||||
class="text-center cursor-pointer border-none bg-none opacity-70 sm:w-[calc(33.3333%-15px) w-[calc(100%/3-15px)] md:w-[calc(100%/4-15px)]"
|
||||
>
|
||||
<img
|
||||
class="country-flag w-full h-auto"
|
||||
src="/flags/${country.code}.svg"
|
||||
/>
|
||||
<span class="country-name">${country.name}</span>
|
||||
</button>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
updated() {
|
||||
const preview = this.renderRoot.querySelector(
|
||||
"#flag-preview",
|
||||
) as HTMLElement;
|
||||
if (!preview) return;
|
||||
|
||||
preview.innerHTML = "";
|
||||
|
||||
if (this.flag?.startsWith("!")) {
|
||||
renderPlayerFlag(this.flag, preview);
|
||||
} else {
|
||||
const img = document.createElement("img");
|
||||
img.src = this.flag ? `/flags/${this.flag}.svg` : `/flags/xx.svg`;
|
||||
img.style.width = "100%";
|
||||
img.style.height = "100%";
|
||||
img.style.objectFit = "contain";
|
||||
img.onerror = () => {
|
||||
if (!img.src.endsWith("/flags/xx.svg")) {
|
||||
img.src = "/flags/xx.svg";
|
||||
}
|
||||
};
|
||||
preview.appendChild(img);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, query, state } from "lit/decorators.js";
|
||||
import Countries from "./data/countries.json";
|
||||
|
||||
@customElement("flag-input-modal")
|
||||
export class FlagInputModal extends LitElement {
|
||||
@query("o-modal") private modalEl!: HTMLElement & {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
};
|
||||
|
||||
@state() private search: string = "";
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<o-modal title="Flag Selector Modal" alwaysMaximized>
|
||||
<input
|
||||
class="h-[2rem] border-none text-center border border-gray-300 rounded-xl shadow-sm text-2xl text-center focus:outline-none 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"
|
||||
type="text"
|
||||
placeholder="Search..."
|
||||
@change=${this.handleSearch}
|
||||
@keyup=${this.handleSearch}
|
||||
/>
|
||||
<div
|
||||
class="flex flex-wrap justify-evenly gap-[1rem] overflow-y-auto overflow-x-hidden h-[90%]"
|
||||
>
|
||||
${Countries.filter(
|
||||
(country) =>
|
||||
country.name.toLowerCase().includes(this.search.toLowerCase()) ||
|
||||
country.code.toLowerCase().includes(this.search.toLowerCase()),
|
||||
).map(
|
||||
(country) => html`
|
||||
<button
|
||||
@click=${() => {
|
||||
this.setFlag(country.code);
|
||||
this.close();
|
||||
}}
|
||||
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-[80px]"
|
||||
>
|
||||
<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>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
</o-modal>
|
||||
`;
|
||||
}
|
||||
|
||||
private handleSearch(event: Event) {
|
||||
this.search = (event.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private setFlag(flag: string) {
|
||||
localStorage.setItem("flag", flag);
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("flag-change", {
|
||||
detail: { flag },
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public open() {
|
||||
this.modalEl?.open();
|
||||
}
|
||||
public close() {
|
||||
this.modalEl?.close();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import "./DarkModeButton";
|
||||
import { DarkModeButton } from "./DarkModeButton";
|
||||
import "./FlagInput";
|
||||
import { FlagInput } from "./FlagInput";
|
||||
import { FlagInputModal } from "./FlagInputModal";
|
||||
import { GameStartingModal } from "./GameStartingModal";
|
||||
import "./GoogleAdElement";
|
||||
import { HelpModal } from "./HelpModal";
|
||||
@@ -194,6 +195,16 @@ class Client {
|
||||
hlpModal.open();
|
||||
});
|
||||
|
||||
const flagInputModal = document.querySelector(
|
||||
"flag-input-modal",
|
||||
) as FlagInputModal;
|
||||
flagInputModal instanceof FlagInputModal;
|
||||
const flgInput = document.getElementById("flag-input_");
|
||||
if (flgInput === null) throw new Error("Missing flag-input_");
|
||||
flgInput.addEventListener("click", () => {
|
||||
flagInputModal.open();
|
||||
});
|
||||
|
||||
const territoryModal = document.querySelector(
|
||||
"territory-patterns-modal",
|
||||
) as TerritoryPatternsModal;
|
||||
|
||||
@@ -7,6 +7,7 @@ export class OModal extends LitElement {
|
||||
@state() public isModalOpen = false;
|
||||
@property({ type: String }) title = "";
|
||||
@property({ type: String }) translationKey = "";
|
||||
@property({ type: Boolean }) alwaysMaximized = false;
|
||||
|
||||
static styles = css`
|
||||
.c-modal {
|
||||
@@ -31,6 +32,17 @@ export class OModal extends LitElement {
|
||||
max-width: 860px;
|
||||
}
|
||||
|
||||
.c-modal__wrapper.always-maximized {
|
||||
width: 100%;
|
||||
min-width: 340px;
|
||||
max-width: 860px;
|
||||
min-height: 320px;
|
||||
/* Fallback for older browsers */
|
||||
height: 60vh;
|
||||
/* Use dvh if supported for dynamic viewport handling */
|
||||
height: 60dvh;
|
||||
}
|
||||
|
||||
.c-modal__header {
|
||||
position: relative;
|
||||
border-top-left-radius: 4px;
|
||||
@@ -74,7 +86,11 @@ export class OModal extends LitElement {
|
||||
${this.isModalOpen
|
||||
? html`
|
||||
<aside class="c-modal">
|
||||
<div class="c-modal__wrapper">
|
||||
<div
|
||||
class="c-modal__wrapper ${this.alwaysMaximized
|
||||
? "always-maximized"
|
||||
: ""}"
|
||||
>
|
||||
<header class="c-modal__header">
|
||||
${`${this.translationKey}` === ""
|
||||
? `${this.title}`
|
||||
|
||||
@@ -411,6 +411,7 @@
|
||||
<news-modal></news-modal>
|
||||
<game-left-sidebar></game-left-sidebar>
|
||||
<spawn-ad></spawn-ad>
|
||||
<flag-input-modal></flag-input-modal>
|
||||
<fps-display></fps-display>
|
||||
<div
|
||||
id="language-modal"
|
||||
|
||||
Reference in New Issue
Block a user