diff --git a/resources/lang/bg.json b/resources/lang/bg.json index 8377226a0..44a259566 100644 --- a/resources/lang/bg.json +++ b/resources/lang/bg.json @@ -5,6 +5,7 @@ "join_lobby": "Присъединяване към частна игра", "single_player": "Самостоятелна игра", "instructions": "Инструкции", + "settings": "настройки", "how_to_play": "Как се играе", "wiki": "Уики" }, diff --git a/resources/lang/bn.json b/resources/lang/bn.json index 64d24d666..083c6deb6 100644 --- a/resources/lang/bn.json +++ b/resources/lang/bn.json @@ -5,6 +5,7 @@ "join_lobby": "লবিতে যোগ দিন", "single_player": "কম্পিউটার মোড", "instructions": "নির্দেশনা", + "settings": "সেটিংস", "how_to_play": "খেলার নিয়ম", "wiki": "উইকি" }, diff --git a/resources/lang/de.json b/resources/lang/de.json index 887b23dbd..812c0421a 100644 --- a/resources/lang/de.json +++ b/resources/lang/de.json @@ -5,6 +5,7 @@ "join_lobby": "Lobby beitreten", "single_player": "Einzelspieler", "instructions": "Anleitung", + "settings": "Einstellungen", "how_to_play": "Wie man Spielt", "wiki": "Wiki" }, diff --git a/resources/lang/debug.json b/resources/lang/debug.json index 0e6eaf3ca..19458ae51 100644 --- a/resources/lang/debug.json +++ b/resources/lang/debug.json @@ -11,6 +11,7 @@ "join_lobby": "main.join_lobby", "single_player": "main.single_player", "instructions": "main.instructions", + "settings": "main.settings", "how_to_play": "main.how_to_play", "wiki": "main.wiki" }, diff --git a/resources/lang/en.json b/resources/lang/en.json index ee02e26c5..56c1d158d 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -12,6 +12,7 @@ "join_lobby": "Join Lobby", "single_player": "Single Player", "instructions": "Instructions", + "settings": "Settings", "how_to_play": "How to Play", "wiki": "Wiki" }, diff --git a/resources/lang/eo.json b/resources/lang/eo.json index b64c252e1..bb17cd124 100644 --- a/resources/lang/eo.json +++ b/resources/lang/eo.json @@ -5,6 +5,7 @@ "join_lobby": "Kunigi salonon", "single_player": "Sola Ludanto", "instructions": "Instrukcioj", + "settings": "agordojn", "how_to_play": "Kiel ludi", "wiki": "Vikio" }, diff --git a/resources/lang/es.json b/resources/lang/es.json index 811eca357..7f6067f43 100644 --- a/resources/lang/es.json +++ b/resources/lang/es.json @@ -11,6 +11,7 @@ "join_lobby": "Unirse a una Partida Privada", "single_player": "Un jugador", "instructions": "Instrucciones", + "settings": "ajustes", "how_to_play": "Cómo jugar", "wiki": "Wiki" }, diff --git a/resources/lang/fr.json b/resources/lang/fr.json index 1a84cefab..630164fa8 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -5,6 +5,7 @@ "join_lobby": "Rejoindre un salon", "single_player": "Mode solo", "instructions": "Instructions", + "settings": "paramètres", "how_to_play": "Comment jouer ?", "wiki": "Wiki" }, diff --git a/resources/lang/hi.json b/resources/lang/hi.json index bf99eb976..c2677275b 100644 --- a/resources/lang/hi.json +++ b/resources/lang/hi.json @@ -5,6 +5,7 @@ "join_lobby": "लॉबी में शामिल हों", "single_player": "कंप्यूटर मोड", "instructions": "निर्देश", + "settings": "सेटिंग्स", "how_to_play": "कैसे खेलें", "wiki": "विकी" }, diff --git a/resources/lang/it.json b/resources/lang/it.json index 5933a810d..891c9f4a7 100644 --- a/resources/lang/it.json +++ b/resources/lang/it.json @@ -5,6 +5,7 @@ "join_lobby": "Unisciti a una Lobby", "single_player": "Giocatore Singolo", "instructions": "Istruzioni", + "settings": "Istruzioni", "how_to_play": "Come si gioca", "wiki": "Wiki" }, diff --git a/resources/lang/ja.json b/resources/lang/ja.json index a86528b71..c6921898e 100644 --- a/resources/lang/ja.json +++ b/resources/lang/ja.json @@ -11,6 +11,7 @@ "join_lobby": "ロビーに参加", "single_player": "シングルプレイヤー", "instructions": "説明書", + "settings": "設定", "how_to_play": "遊び方", "wiki": "ウィキ" }, diff --git a/resources/lang/nl.json b/resources/lang/nl.json index 3029bc5f6..8a566347c 100644 --- a/resources/lang/nl.json +++ b/resources/lang/nl.json @@ -5,6 +5,7 @@ "join_lobby": "Lobby toetreden", "single_player": "Eén Speler", "instructions": "Instructies", + "settings": "instellingen", "how_to_play": "Hoe spelen?", "wiki": "Wiki" }, diff --git a/resources/lang/pl.json b/resources/lang/pl.json index 01fc7a0dd..0541694aa 100644 --- a/resources/lang/pl.json +++ b/resources/lang/pl.json @@ -5,6 +5,7 @@ "join_lobby": "Dołącz do lobby", "single_player": "Gra jednoosobowa", "instructions": "Instrukcje", + "settings": "ustawienia", "how_to_play": "Jak grać", "wiki": "Wiki" }, diff --git a/resources/lang/pt_br.json b/resources/lang/pt_br.json index ba24a59ac..469b3e2a2 100644 --- a/resources/lang/pt_br.json +++ b/resources/lang/pt_br.json @@ -5,6 +5,7 @@ "join_lobby": "Entrar na Sala", "single_player": "Um Jogador", "instructions": "Instruções", + "settings": "configurações", "how_to_play": "Como Jogar", "wiki": "Wiki" }, diff --git a/resources/lang/ru.json b/resources/lang/ru.json index 8a135f9fa..f6d351368 100644 --- a/resources/lang/ru.json +++ b/resources/lang/ru.json @@ -5,6 +5,7 @@ "join_lobby": "Присоединиться к лобби", "single_player": "Одиночная игра", "instructions": "Инструкции", + "settings": "настройки", "how_to_play": "Как играть", "wiki": "Вики" }, diff --git a/resources/lang/sh.json b/resources/lang/sh.json index 8476917f6..9671d6570 100644 --- a/resources/lang/sh.json +++ b/resources/lang/sh.json @@ -5,6 +5,7 @@ "join_lobby": "Pridruži se čekaonici", "single_player": "Igraj sam", "instructions": "Instrukcije", + "settings": "postavke", "how_to_play": "Kako igrati", "wiki": "Wiki" }, diff --git a/resources/lang/tr.json b/resources/lang/tr.json index 37d25f684..b75f90eeb 100644 --- a/resources/lang/tr.json +++ b/resources/lang/tr.json @@ -5,6 +5,7 @@ "join_lobby": "Lobiye Katıl", "single_player": "Tek Oyunculu", "instructions": "Rehber", + "settings": "ayarlar", "how_to_play": "Nasıl Oynanır", "wiki": "Wiki" }, diff --git a/resources/lang/uk.json b/resources/lang/uk.json index 946187b5c..eafced966 100644 --- a/resources/lang/uk.json +++ b/resources/lang/uk.json @@ -5,6 +5,7 @@ "join_lobby": "Приєднатися до лобі", "single_player": "Гра наодинці", "instructions": "Інструкції", + "settings": "налаштування", "how_to_play": "Як грати", "wiki": "Вікі" }, diff --git a/src/client/LangSelector.ts b/src/client/LangSelector.ts deleted file mode 100644 index 8698e8483..000000000 --- a/src/client/LangSelector.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { LitElement, html } from "lit"; -import { customElement, state } from "lit/decorators.js"; -import "./LanguageModal"; - -import bg from "../../resources/lang/bg.json"; -import bn from "../../resources/lang/bn.json"; -import de from "../../resources/lang/de.json"; -import en from "../../resources/lang/en.json"; -import eo from "../../resources/lang/eo.json"; -import es from "../../resources/lang/es.json"; -import fr from "../../resources/lang/fr.json"; -import hi from "../../resources/lang/hi.json"; -import it from "../../resources/lang/it.json"; -import ja from "../../resources/lang/ja.json"; -import nl from "../../resources/lang/nl.json"; -import pl from "../../resources/lang/pl.json"; -import pt_br from "../../resources/lang/pt_br.json"; -import ru from "../../resources/lang/ru.json"; -import sh from "../../resources/lang/sh.json"; -import tr from "../../resources/lang/tr.json"; -import uk from "../../resources/lang/uk.json"; - -@customElement("lang-selector") -export class LangSelector extends LitElement { - @state() public translations: any = {}; - @state() private defaultTranslations: any = {}; - @state() private currentLang: string = "en"; - @state() private languageList: any[] = []; - @state() private showModal: boolean = false; - @state() private debugMode: boolean = false; - - private dKeyPressed: boolean = false; - - private languageMap: Record = { - bg, - bn, - de, - en, - es, - eo, - fr, - it, - hi, - ja, - nl, - pl, - pt_br, - ru, - sh, - tr, - uk, - }; - - createRenderRoot() { - return this; // Use Light DOM if you prefer this - } - - connectedCallback() { - super.connectedCallback(); - this.setupDebugKey(); - this.initializeLanguage(); - } - - private setupDebugKey() { - window.addEventListener("keydown", (e) => { - if (e.key.toLowerCase() === "t") this.dKeyPressed = true; - }); - window.addEventListener("keyup", (e) => { - if (e.key.toLowerCase() === "t") this.dKeyPressed = false; - }); - } - - private async initializeLanguage() { - const locale = new Intl.Locale(navigator.language); - const defaultLang = locale.language; - const userLang = localStorage.getItem("lang") || defaultLang; - - this.defaultTranslations = await this.loadLanguage("en"); - this.translations = await this.loadLanguage(userLang); - this.currentLang = userLang; - - await this.loadLanguageList(); - this.applyTranslation(this.translations); - } - - private async loadLanguage(lang: string): Promise { - return Promise.resolve(this.languageMap[lang] || {}); - } - - private async loadLanguageList() { - try { - const data = this.languageMap; - let list: any[] = []; - - const browserLang = new Intl.Locale(navigator.language).language; - - for (const langCode of Object.keys(data)) { - const langData = data[langCode].lang; - if (!langData) continue; - - list.push({ - code: langData.lang_code ?? langCode, - native: langData.native ?? langCode, - en: langData.en ?? langCode, - svg: langData.svg ?? langCode, - }); - } - - let debugLang: any = null; - if (this.dKeyPressed) { - debugLang = { - code: "debug", - native: "Debug", - en: "Debug", - svg: "xx", - }; - this.debugMode = true; - } - - const currentLangEntry = list.find((l) => l.code === this.currentLang); - const browserLangEntry = - browserLang !== this.currentLang && browserLang !== "en" - ? list.find((l) => l.code === browserLang) - : undefined; - const englishEntry = - this.currentLang !== "en" - ? list.find((l) => l.code === "en") - : undefined; - - list = list.filter( - (l) => - l.code !== this.currentLang && - l.code !== browserLang && - l.code !== "en" && - l.code !== "debug", - ); - - list.sort((a, b) => a.en.localeCompare(b.en)); - - const finalList: any[] = []; - if (currentLangEntry) finalList.push(currentLangEntry); - if (englishEntry) finalList.push(englishEntry); - if (browserLangEntry) finalList.push(browserLangEntry); - finalList.push(...list); - if (debugLang) finalList.push(debugLang); - - this.languageList = finalList; - } catch (err) { - console.error("Failed to load language list:", err); - } - } - - private async changeLanguage(lang: string) { - localStorage.setItem("lang", lang); - this.translations = await this.loadLanguage(lang); - this.currentLang = lang; - this.applyTranslation(this.translations); - this.showModal = false; - } - - private applyTranslation(translations: any) { - const components = [ - "single-player-modal", - "host-lobby-modal", - "join-private-lobby-modal", - "emoji-table", - "leader-board", - "build-menu", - "win-modal", - "game-starting-modal", - "top-bar", - "player-panel", - "help-modal", - "username-input", - "public-lobby", - "o-modal", - "o-button", - ]; - - document.title = translations.main?.title || document.title; - - document.querySelectorAll("[data-i18n]").forEach((element) => { - const key = element.getAttribute("data-i18n"); - const keys = key?.split(".") || []; - let text = translations; - - for (const k of keys) { - text = text?.[k]; - if (!text) break; - } - - if (!text && this.defaultTranslations) { - let fallback = this.defaultTranslations; - for (const k of keys) { - fallback = fallback?.[k]; - if (!fallback) break; - } - text = fallback; - } - - if (text) { - element.innerHTML = text; - } else { - console.warn(`Translation key not found: ${key}`); - } - }); - - components.forEach((tag) => { - document.querySelectorAll(tag).forEach((el) => { - if (typeof (el as any).requestUpdate === "function") { - (el as any).requestUpdate(); - } - }); - }); - } - - public translateText( - key: string, - params: Record = {}, - ): string { - const keys = key.split("."); - let text: any = this.translations; - - for (const k of keys) { - text = text?.[k]; - if (!text) break; - } - - if (!text && this.defaultTranslations) { - text = this.defaultTranslations; - for (const k of keys) { - text = text?.[k]; - if (!text) return key; - } - } - - for (const [param, value] of Object.entries(params)) { - text = text.replace(`{${param}}`, String(value)); - } - - return text; - } - - private openModal() { - this.debugMode = this.dKeyPressed; - this.showModal = true; - this.loadLanguageList(); - } - - render() { - const currentLang = - this.languageList.find((l) => l.code === this.currentLang) ?? - (this.currentLang === "debug" - ? { - code: "debug", - native: "Debug", - en: "Debug", - svg: "xx", - } - : { - native: "English", - en: "English", - svg: "uk_us_flag", - }); - - return html` -
- -
- - - this.changeLanguage(e.detail.lang)} - @close-modal=${() => (this.showModal = false)} - > - `; - } -} diff --git a/src/client/Main.ts b/src/client/Main.ts index 567a6673b..2194a0111 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -16,9 +16,6 @@ import GoogleAdElement from "./GoogleAdElement"; import { HelpModal } from "./HelpModal"; import { HostLobbyModal as HostPrivateLobbyModal } from "./HostLobbyModal"; import { JoinPrivateLobbyModal } from "./JoinPrivateLobbyModal"; -import "./LangSelector"; -import { LangSelector } from "./LangSelector"; -import { LanguageModal } from "./LanguageModal"; import "./PublicLobby"; import { PublicLobby } from "./PublicLobby"; import { SinglePlayerModal } from "./SinglePlayerModal"; @@ -28,6 +25,7 @@ import { UsernameInput } from "./UsernameInput"; import { generateCryptoRandomUUID } from "./Utils"; import "./components/baseComponents/Button"; import "./components/baseComponents/Modal"; +import "./components/baseComponents/Select"; import "./styles.css"; export interface JoinLobbyEvent { @@ -55,19 +53,6 @@ class Client { constructor() {} initialize(): void { - const langSelector = document.querySelector( - "lang-selector", - ) as LangSelector; - const LanguageModal = document.querySelector( - "lang-selector", - ) as LanguageModal; - if (!langSelector) { - consolex.warn("Lang selector element not found"); - } - if (!LanguageModal) { - consolex.warn("Language modal element not found"); - } - this.flagInput = document.querySelector("flag-input") as FlagInput; if (!this.flagInput) { consolex.warn("Flag input element not found"); diff --git a/src/client/UserSettingModal.ts b/src/client/UserSettingModal.ts index 8c065a1f7..3fb746338 100644 --- a/src/client/UserSettingModal.ts +++ b/src/client/UserSettingModal.ts @@ -1,6 +1,24 @@ import { LitElement, html } from "lit"; import { customElement, query, state } from "lit/decorators.js"; +import bg from "../../resources/lang/bg.json"; +import bn from "../../resources/lang/bn.json"; +import de from "../../resources/lang/de.json"; +import en from "../../resources/lang/en.json"; +import eo from "../../resources/lang/eo.json"; +import es from "../../resources/lang/es.json"; +import fr from "../../resources/lang/fr.json"; +import hi from "../../resources/lang/hi.json"; +import it from "../../resources/lang/it.json"; +import ja from "../../resources/lang/ja.json"; +import nl from "../../resources/lang/nl.json"; +import pl from "../../resources/lang/pl.json"; +import pt_br from "../../resources/lang/pt_br.json"; +import ru from "../../resources/lang/ru.json"; +import sh from "../../resources/lang/sh.json"; +import tr from "../../resources/lang/tr.json"; +import uk from "../../resources/lang/uk.json"; import { UserSettings } from "../core/game/UserSettings"; +import "./components/baseComponents/Select"; import "./components/baseComponents/setting/SettingNumber"; import "./components/baseComponents/setting/SettingSlider"; import "./components/baseComponents/setting/SettingToggle"; @@ -10,29 +28,69 @@ export class UserSettingModal extends LitElement { private userSettings: UserSettings = new UserSettings(); @state() private darkMode: boolean = this.userSettings.darkMode(); - + @state() public translations: any = {}; + @state() private defaultTranslations: any = {}; @state() private keySequence: string[] = []; @state() private showEasterEggSettings = false; - connectedCallback() { - super.connectedCallback(); - window.addEventListener("keydown", this.handleKeyDown); - } - + @state() private language = localStorage.getItem("lang") || "en"; + @state() private languageList: any[] = []; @query("o-modal") private modalEl!: HTMLElement & { open: () => void; close: () => void; isModalOpen: boolean; }; - createRenderRoot() { - return this; + private languageMap: Record = { + bg, + bn, + de, + en, + eo, + es, + fr, + hi, + it, + ja, + nl, + pl, + pt_br, + ru, + sh, + tr, + uk, + }; + + private async loadLanguageList() { + const data = this.languageMap; + const list: any[] = []; + + for (const langCode of Object.keys(data)) { + const langData = data[langCode].lang; + if (!langData) continue; + list.push({ + code: langData.lang_code ?? langCode, + native: langData.native ?? langCode, + en: langData.en ?? langCode, + svg: langData.svg ?? langCode, + }); + } + + list.sort((a, b) => a.en.localeCompare(b.en)); + this.languageList = list; } - disconnectedCallback() { - window.removeEventListener("keydown", this.handleKeyDown); - super.disconnectedCallback(); - document.body.style.overflow = "auto"; + private async initializeLanguage() { + const locale = new Intl.Locale(navigator.language); + const defaultLang = locale.language; + const userLang = localStorage.getItem("lang") || defaultLang; + + this.defaultTranslations = await this.loadLanguage("en"); + this.translations = await this.loadLanguage(userLang); + this.language = userLang; + + await this.loadLanguageList(); + this.applyTranslation(this.translations); } private handleKeyDown = (e: KeyboardEvent) => { @@ -49,7 +107,6 @@ export class UserSettingModal extends LitElement { }; private triggerEasterEgg() { - console.log("🪺 Setting~ unlocked by EVAN combo!"); this.showEasterEggSettings = true; const popup = document.createElement("div"); popup.className = "easter-egg-popup"; @@ -61,23 +118,65 @@ export class UserSettingModal extends LitElement { }, 5000); } - toggleDarkMode(e: CustomEvent<{ checked: boolean }>) { - const enabled = e.detail?.checked; + private applyTranslation(translations: any) { + const components = [ + "single-player-modal", + "host-lobby-modal", + "join-private-lobby-modal", + "emoji-table", + "leader-board", + "build-menu", + "win-modal", + "game-starting-modal", + "top-bar", + "player-panel", + "help-modal", + "username-input", + "public-lobby", + "user-setting", + "setting-slider", + "setting-number", + "setting-number", + "o-modal", + "o-button", + "o-select", + ]; - if (typeof enabled !== "boolean") { - console.warn("Unexpected toggle event payload", e); - return; - } + document.title = translations.main?.title || document.title; - this.userSettings.set("settings.darkMode", enabled); + document.querySelectorAll("[data-i18n]").forEach((element) => { + const key = element.getAttribute("data-i18n"); + const keys = key?.split(".") || []; + let text = translations; - if (enabled) { - document.documentElement.classList.add("dark"); - } else { - document.documentElement.classList.remove("dark"); - } + for (const k of keys) { + text = text?.[k]; + if (!text) break; + } - console.log("🌙 Dark Mode:", enabled ? "ON" : "OFF"); + if (!text && this.defaultTranslations) { + let fallback = this.defaultTranslations; + for (const k of keys) { + fallback = fallback?.[k]; + if (!fallback) break; + } + text = fallback; + } + + if (text) { + element.innerHTML = text; + } else { + console.warn(`Translation key not found: ${key}`); + } + }); + + components.forEach((tag) => { + document.querySelectorAll(tag).forEach((el) => { + if (typeof (el as any).requestUpdate === "function") { + (el as any).requestUpdate(); + } + }); + }); } private toggleEmojis(e: CustomEvent<{ checked: boolean }>) { @@ -85,8 +184,6 @@ export class UserSettingModal extends LitElement { if (typeof enabled !== "boolean") return; this.userSettings.set("settings.emojis", enabled); - - console.log("🤡 Emojis:", enabled ? "ON" : "OFF"); } private toggleLeftClickOpensMenu(e: CustomEvent<{ checked: boolean }>) { @@ -94,7 +191,6 @@ export class UserSettingModal extends LitElement { if (typeof enabled !== "boolean") return; this.userSettings.set("settings.leftClickOpensMenu", enabled); - console.log("🖱️ Left Click Opens Menu:", enabled ? "ON" : "OFF"); this.requestUpdate(); } @@ -119,101 +215,59 @@ export class UserSettingModal extends LitElement { } } - render() { - return html` - - - - `; + public translateText( + key: string, + params: Record = {}, + ): string { + const keys = key.split("."); + let text: any = this.translations; + + for (const k of keys) { + text = text?.[k]; + if (!text) break; + } + + if (!text && this.defaultTranslations) { + text = this.defaultTranslations; + for (const k of keys) { + text = text?.[k]; + if (!text) return key; + } + } + + for (const [param, value] of Object.entries(params)) { + text = text.replace(`{${param}}`, String(value)); + } + + return text; } public open() { @@ -223,4 +277,125 @@ export class UserSettingModal extends LitElement { public close() { this.modalEl?.close(); } + + createRenderRoot() { + return this; + } + + connectedCallback() { + super.connectedCallback(); + this.loadLanguageList(); + this.initializeLanguage(); + window.addEventListener("keydown", this.handleKeyDown); + } + + disconnectedCallback() { + window.removeEventListener("keydown", this.handleKeyDown); + super.disconnectedCallback(); + document.body.style.overflow = "auto"; + } + + render() { + return html` + +
+ ({ + label: `${l.native} (${l.en})`, + value: l.code, + image: `/flags/${l.svg}.svg`, + }))} + .selectedValue=${this.language} + .showImageWithLabel=${true} + @o-select-change=${(e: CustomEvent) => + this.changeLanguage(e.detail)} + > + + ) => + this.toggleDarkMode(e)} + > + + + + + + + + + + ${this.showEasterEggSettings + ? html` + { + const value = e.detail?.value; + if (typeof value === "undefined") { + console.warn("Slider event missing detail.value", e); + } + }} + > + + { + const value = e.detail?.value; + if (typeof value === "undefined") { + console.warn("Slider event missing detail.value", e); + } + }} + > + ` + : null} +
+
+ `; + } } diff --git a/src/client/Utils.ts b/src/client/Utils.ts index a0d3ff912..68c563c9a 100644 --- a/src/client/Utils.ts +++ b/src/client/Utils.ts @@ -1,4 +1,4 @@ -import { LangSelector } from "./LangSelector"; +import { UserSettingModal } from "./UserSettingModal"; export function renderTroops(troops: number): string { return renderNumber(troops / 10); @@ -78,7 +78,9 @@ export const translateText = ( key: string, params: Record = {}, ): string => { - const langSelector = document.querySelector("lang-selector") as LangSelector; + const langSelector = document.querySelector( + "user-setting", + ) as UserSettingModal; if (!langSelector) { console.warn("LangSelector not found in DOM"); return key; diff --git a/src/client/components/baseComponents/Modal.ts b/src/client/components/baseComponents/Modal.ts index a8263dcb6..a00fad045 100644 --- a/src/client/components/baseComponents/Modal.ts +++ b/src/client/components/baseComponents/Modal.ts @@ -39,14 +39,21 @@ export class OModal extends LitElement { background: #000000a1; text-align: center; color: #fff; - padding: 1rem 2.4rem 1rem 1.4rem; + padding: 0.6rem 2.4rem 0.6rem 1.4rem; + + @media (min-width: 1024px) { + padding: 1rem 2.4rem 1rem 1.4rem; + } } .c-modal__close { cursor: pointer; position: absolute; right: 1rem; - top: 1rem; + top: 0.6rem; + @media (min-width: 1024px) { + top: 1rem; + } } .c-modal__content { diff --git a/src/client/components/baseComponents/Select.ts b/src/client/components/baseComponents/Select.ts new file mode 100644 index 000000000..a3034f0ab --- /dev/null +++ b/src/client/components/baseComponents/Select.ts @@ -0,0 +1,260 @@ +import { LitElement, css, html } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { translateText } from "../../Utils"; + +interface selectItems { + label: string; + value: any; + image?: string; +} + +@customElement("o-select") +export class OSelect extends LitElement { + /** + * Array of selectable items. + */ + @property({ type: Array }) items: selectItems[] = []; + + /** + * Currently selected value. + */ + @property({ type: String }) selectedValue: string = ""; + + /** + * Error message to display. + */ + @property({ type: String }) errorMessage: string = ""; + + /** + * Enables search filtering in the dropdown. + */ + @property({ type: Boolean }) filterEnabled: boolean = false; + + /** + * If true, show image next to label. + */ + @property({ type: Boolean }) showImageWithLabel: boolean = false; + + /** + * Label shown above the select. + */ + @property({ type: String }) label: string = ""; + @property({ type: String }) translationKey = ""; + + @state() private selectedItem: selectItems | null = null; + @state() private filter: string = ""; + @state() private isOpen: boolean = false; + + static styles = css` + .c-label { + color: var(--fontColorLight); + font-size: 14px; + } + + .c-select { + position: relative; + cursor: pointer; + padding: 0 10px; + border: 1px solid transparent; + border-radius: var(--borderRadius--md); + background: #1e1e1e; + height: 50px; + + &.is-error { + border-color: var(--errorColor); + + + .c-message { + color: var(--errorColor); + } + } + } + + .c-select__display { + align-items: center; + color: var(--fontColorLight); + width: 100%; + display: flex; + gap: 10px; + + span { + flex: 1; + } + + img { + width: 24px; + } + } + + .c-select__listWrapper { + border: 1px solid var(--secondaryBorderColor); + position: absolute; + left: -1px; + right: -1px; + background: var(--boxBackgroundColor); + backdrop-filter: blur(var(--blur-md)); + top: 50px; + z-index: 1000; + } + + .c-select__list { + list-style: none; + overflow: scroll; + max-height: 35dvh; + display: flex; + flex-direction: column; + gap: 10px; + padding: 5px; + margin: 0; + } + + .c-select__item { + align-items: center; + display: flex; + min-height: 35px; + color: var(--fontColorLight); + gap: 10px; + + img { + max-width: 35px; + height: auto; + } + } + + .c-select__input { + outline: none; + padding: 4px 8px; + margin-bottom: 5px; + } + `; + + connectedCallback() { + super.connectedCallback(); + window.addEventListener("click", this._handleOutsideClick); + } + + disconnectedCallback() { + super.disconnectedCallback(); + window.removeEventListener("click", this._handleOutsideClick); + } + + willUpdate(changedProps: Map) { + if (changedProps.has("items") || changedProps.has("selectedValue")) { + const match = this.items.find( + (item) => item.value === this.selectedValue, + ); + if (match) { + this.selectedItem = match; + } + } + } + + private _handleOutsideClick = (event: MouseEvent) => { + if (!this.contains(event.target as Node)) { + this.isOpen = false; + } + }; + + private selectItem(item: selectItems) { + this.selectedItem = item; + this.filter = ""; + this.isOpen = false; + this.dispatchEvent( + new CustomEvent("o-select-change", { + detail: item.value, + bubbles: true, + composed: true, + }), + ); + } + + private renderSelectedDisplay() { + if (!this.selectedItem) { + return html`Select`; + } + + const { image, label } = this.selectedItem; + + if (this.showImageWithLabel) { + return html` + ${image ? html`${label} flag` : null} + ${label} + `; + } + + return image + ? html`${label} flag` + : html`${label}`; + } + + get filteredItems() { + return this.items.filter((item) => + item.label.toLowerCase().includes(this.filter.toLowerCase()), + ); + } + + render() { + return html` +
(this.isOpen = !this.isOpen)} + > + ${this.label + ? html`` + : null} +
${this.renderSelectedDisplay()}
+ + ${this.isOpen + ? html` +
+ ${this.filterEnabled + ? html` + { + this.filter = (e.target as HTMLInputElement).value; + e.stopPropagation(); + e.stopImmediatePropagation(); + }} + @click=${(e: Event) => e.stopPropagation()} + /> + ` + : null} +
    + ${this.filteredItems.map( + (item) => html` +
  • { + this.selectItem(item); + e.stopPropagation(); + e.stopImmediatePropagation(); + }} + > + ${item.image + ? html`${item.label}` + : ""} +
    ${item.label}
    +
  • + `, + )} +
+
+ ` + : ""} +
+ ${this.errorMessage + ? html`
${this.errorMessage}
` + : ""} + `; + } +} diff --git a/src/client/index.html b/src/client/index.html index f0b8810f3..1b4b4f881 100644 --- a/src/client/index.html +++ b/src/client/index.html @@ -263,26 +263,15 @@ block secondary > -
- -
+ - - - -
    @@ -359,24 +348,6 @@ -