mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 10:53:31 +00:00
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:
+71
-521
@@ -127,552 +127,102 @@
|
||||
|
||||
<div
|
||||
id="mobile-menu-backdrop"
|
||||
class="lg:!hidden [.in-game_&]:hidden hidden pointer-events-none [&.open]:block [&.open]:pointer-events-auto [&.open]:fixed [&.open]:inset-0 [&.open]:bg-black/50 [&.open]:z-[40000] transition-opacity"
|
||||
class="lg:!hidden [.in-game_&]:hidden hidden pointer-events-none [&.open]:block [&.open]:pointer-events-auto [&.open]:fixed [&.open]:inset-0 [&.open]:bg-black/60 [&.open]:z-[40000] transition-opacity"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
<div
|
||||
|
||||
<mobile-nav-bar
|
||||
id="sidebar-menu"
|
||||
class="peer [.in-game_&]:hidden z-[40001] fixed left-0 top-0 h-full flex flex-col justify-start overflow-visible bg-black/60 backdrop-blur-md transition-transform duration-500 ease-out transform -translate-x-full w-[80%] [&.open]:translate-x-0 lg:hidden"
|
||||
role="dialog"
|
||||
data-i18n-aria-label="main.menu"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<!-- Border Segments (Custom right border with gap for button) -->
|
||||
<div
|
||||
class="absolute right-0 top-0 w-px bg-transparent"
|
||||
style="height: calc(50% - 64px)"
|
||||
></div>
|
||||
<div
|
||||
class="absolute right-0 bottom-0 w-px bg-transparent"
|
||||
style="height: calc(50% - 64px)"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="flex-1 w-full flex flex-col justify-start overflow-y-auto md:pt-[clamp(1rem,3vh,4rem)] md:pb-[clamp(0.5rem,2vh,2rem)] md:px-[clamp(1rem,1.5vw,2rem)] p-5 gap-[clamp(1rem,3vh,3rem)]"
|
||||
>
|
||||
<!-- Logo + Menu -->
|
||||
<div
|
||||
class="flex flex-col text-[#2563eb] mb-[clamp(1rem,2vh,2rem)] ml-[clamp(0.2rem,0.4vw,0.4vh)]"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1364 259"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="currentColor"
|
||||
class="w-[clamp(120px,15vw,192px)] h-[clamp(40px,6vh,64px)] drop-shadow-[0_0_10px_rgba(37,99,235,0.3)]"
|
||||
>
|
||||
<!-- (Logo paths preserved) -->
|
||||
<path
|
||||
d="M0,174V51h15.24v-17.14h16.81v-16.98h16.96V0h1266v17.23h17.13v16.81h16.98v16.96h14.88v123h-15.13v17.08h-17.08v17.08h-16.9v17.04H324.9v16.86h-16.9v16.95h-102v-17.12h-17.07v-17.05H48.73v-17.05h-16.89v-16.89H14.94v-16.89H0ZM1297.95,17.35H65.9v16.7h-17.08v17.08h-14.5v123.08h14.85v16.9h17.08v17.08h139.9v17.08h17.08v16.36h67.9v-16.72h17.08v-17.07h989.88v-17.07h17.08v-16.9h14.44V50.8h-14.75v-17.08h-16.9v-16.37Z"
|
||||
/>
|
||||
<path
|
||||
d="M189.1,154.78v17.07h-16.9v16.75h-51.07v-16.42h-16.9v-17.07h-16.97v-84.88h16.63v-17.07h16.9v-16.84h51.07v16.5h17.07v17.07h16.7v84.89h-16.54ZM137.87,53.1v17.15h-16.6v84.86h16.97v16.61h16.89v-16.97h16.6v-84.86h-16.97v-16.79h-16.89Z"
|
||||
/>
|
||||
<path
|
||||
d="M273.91,104.06v-16.73h50.92v16.45h16.85v68.05h-16.44v17.06h-50.96v16.88h16.4v16.96h-67.28v-16.61h16.33v-101.86h-16.38v-16.98h33.4v16.63c6.12,0,11.72,0,17.31,0,0,22.56,0,45.13,0,67.75h33.59v-67.61h-33.73Z"
|
||||
/>
|
||||
<path
|
||||
d="M631.12,188.64v-16.36h16.53V53.2h-16.25v-16.86h118.33v33.29h-16.65v-16.36h-50.72v50.44h33.36v-16.35h16.99v50.25h-16.6v-16.33h-33.73v50.65h16.37v16.72h-67.63Z"
|
||||
/>
|
||||
<path
|
||||
d="M596.78,103.8v84.94h-33.54v-84.39h-34.03v84.25h-33.85v-101.29h84.5v16.49h16.93Z"
|
||||
/>
|
||||
<path
|
||||
d="M1107.12,188.71v-84.34h-34.03v84.37h-33.7v-101.41h84.42v16.41h16.86v84.96h-33.54Z"
|
||||
/>
|
||||
<path
|
||||
d="M988.1,171.78v16.87h-67.88v-16.38h-16.87v-68.06h16.38v-16.87h68.06v16.38h16.87v68.06h-16.55ZM970.78,104.35h-33.39v67.38h33.39v-67.38Z"
|
||||
/>
|
||||
<path
|
||||
d="M460.77,155.38v16.49h-16.58v16.83h-68.05v-16.5h-16.83v-68.05h16.49v-16.83h68.05v16.49h16.83v34.06h-67.31v33.82h33.47v-16.31h33.92ZM393.39,104.18v16.56h33.3v-16.56h-33.3Z"
|
||||
/>
|
||||
<path
|
||||
d="M1209.13,172h-16.9v-67.9h-16.96v-16.9h16.68v-17.08h16.9v-16.82h16.9v33.58h50.98v16.91h-50.4v67.96h16.48v-16.43h50.95v16.54h-16.55v16.76h-68.08v-16.6Z"
|
||||
/>
|
||||
<path
|
||||
d="M834.91,120.94v16.96h-16.65v33.88h16.41v16.96h-67.29v-16.63h16.34v-67.87h-16.4v-16.97h50.42v33.81h17.3l-.14-.14Z"
|
||||
/>
|
||||
<path
|
||||
d="M835.05,121.08v-33.75h33.76v16.43h16.85v33.96h-33.43v-16.79c-6.13,0-11.73,0-17.32,0,0,0,.14.14.14.14Z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
id="game-version"
|
||||
class="l-header__highlightText text-center"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-play"
|
||||
data-i18n="main.play"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-news"
|
||||
data-i18n="main.news"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-item-store"
|
||||
data-i18n="main.store"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-options"
|
||||
data-i18n="main.options"
|
||||
></button>
|
||||
<!-- Keybinds menu item removed for mobile users -->
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-stats"
|
||||
data-i18n="main.stats"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-account"
|
||||
data-i18n="main.account"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-help"
|
||||
data-i18n="main.help"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 w-full mt-auto">
|
||||
<div
|
||||
class="[.in-game_&]:hidden flex w-full items-center justify-end gap-4 pt-4 border-t border-white/10"
|
||||
>
|
||||
<div class="flex-shrink-0">
|
||||
<lang-selector></lang-selector>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
></mobile-nav-bar>
|
||||
|
||||
<!-- MAIN CONTENT AREA -->
|
||||
<div
|
||||
class="[.in-game_&]:hidden flex-1 relative overflow-hidden h-full transition-[margin] duration-500 ease-out will-change-[margin-left] flex flex-col"
|
||||
>
|
||||
<!-- Desktop Top Bar -->
|
||||
<nav
|
||||
class="hidden lg:flex w-full bg-slate-950/70 backdrop-blur-md border-b border-white/10 items-center justify-center gap-8 py-4 shrink-0 transition-opacity z-50 relative"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="h-8 text-[#2563eb]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1364 259"
|
||||
fill="currentColor"
|
||||
class="h-full w-auto drop-shadow-[0_0_10px_rgba(37,99,235,0.4)]"
|
||||
>
|
||||
<path
|
||||
d="M0,174V51h15.24v-17.14h16.81v-16.98h16.96V0h1266v17.23h17.13v16.81h16.98v16.96h14.88v123h-15.13v17.08h-17.08v17.08h-16.9v17.04H324.9v16.86h-16.9v16.95h-102v-17.12h-17.07v-17.05H48.73v-17.05h-16.89v-16.89H14.94v-16.89H0ZM1297.95,17.35H65.9v16.7h-17.08v17.08h-14.5v123.08h14.85v16.9h17.08v17.08h139.9v17.08h17.08v16.36h67.9v-16.72h17.08v-17.07h989.88v-17.07h17.08v-16.9h14.44V50.8h-14.75v-17.08h-16.9v-16.37Z"
|
||||
/>
|
||||
<path
|
||||
d="M189.1,154.78v17.07h-16.9v16.75h-51.07v-16.42h-16.9v-17.07h-16.97v-84.88h16.63v-17.07h16.9v-16.84h51.07v16.5h17.07v17.07h16.7v84.89h-16.54ZM137.87,53.1v17.15h-16.6v84.86h16.97v16.61h16.89v-16.97h16.6v-84.86h-16.97v-16.79h-16.89Z"
|
||||
/>
|
||||
<path
|
||||
d="M273.91,104.06v-16.73h50.92v16.45h16.85v68.05h-16.44v17.06h-50.96v16.88h16.4v16.96h-67.28v-16.61h16.33v-101.86h-16.38v-16.98h33.4v16.63c6.12,0,11.72,0,17.31,0,0,22.56,0,45.13,0,67.75h33.59v-67.61h-33.73Z"
|
||||
/>
|
||||
<path
|
||||
d="M631.12,188.64v-16.36h16.53V53.2h-16.25v-16.86h118.33v33.29h-16.65v-16.36h-50.72v50.44h33.36v-16.35h16.99v50.25h-16.6v-16.33h-33.73v50.65h16.37v16.72h-67.63Z"
|
||||
/>
|
||||
<path
|
||||
d="M596.78,103.8v84.94h-33.54v-84.39h-34.03v84.25h-33.85v-101.29h84.5v16.49h16.93Z"
|
||||
/>
|
||||
<path
|
||||
d="M1107.12,188.71v-84.34h-34.03v84.37h-33.7v-101.41h84.42v16.41h16.86v84.96h-33.54Z"
|
||||
/>
|
||||
<path
|
||||
d="M988.1,171.78v16.87h-67.88v-16.38h-16.87v-68.06h16.38v-16.87h68.06v16.38h16.87v68.06h-16.55ZM970.78,104.35h-33.39v67.38h33.39v-67.38Z"
|
||||
/>
|
||||
<path
|
||||
d="M460.77,155.38v16.49h-16.58v16.83h-68.05v-16.5h-16.83v-68.05h16.49v-16.83h68.05v16.49h16.83v34.06h-67.31v33.82h33.47v-16.31h33.92ZM393.39,104.18v16.56h33.3v-16.56h-33.3Z"
|
||||
/>
|
||||
<path
|
||||
d="M1209.13,172h-16.9v-67.9h-16.96v-16.9h16.68v-17.08h16.9v-16.82h16.9v33.58h50.98v16.91h-50.4v67.96h16.48v-16.43h50.95v16.54h-16.55v16.76h-68.08v-16.6Z"
|
||||
/>
|
||||
<path
|
||||
d="M834.91,120.94v16.96h-16.65v33.88h16.41v16.96h-67.29v-16.63h16.34v-67.87h-16.4v-16.97h50.42v33.81h17.3l-.14-.14Z"
|
||||
/>
|
||||
<path
|
||||
d="M835.05,121.08v-33.75h33.76v16.43h16.85v33.96h-33.43v-16.79c-6.13,0-11.73,0-17.32,0,0,0,.14.14.14.14Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
class="game-version-display text-[#2563eb] text-xs font-bold tracking-widest uppercase cursor-default select-text leading-none mt-1"
|
||||
></div>
|
||||
</div>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-play"
|
||||
data-i18n="main.play"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-news"
|
||||
data-i18n="main.news"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500 relative"
|
||||
data-page="page-item-store"
|
||||
data-i18n="main.store"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-options"
|
||||
data-i18n="main.options"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-keybinds"
|
||||
data-i18n="main.keys"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-stats"
|
||||
data-i18n="main.stats"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-account"
|
||||
data-i18n="main.account"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-help"
|
||||
data-i18n="main.help"
|
||||
></button>
|
||||
<lang-selector></lang-selector>
|
||||
</nav>
|
||||
<desktop-nav-bar></desktop-nav-bar>
|
||||
|
||||
<div
|
||||
id="turnstile-container"
|
||||
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[99999]"
|
||||
></div>
|
||||
|
||||
<gutter-ads></gutter-ads>
|
||||
|
||||
<!-- Main container with responsive padding -->
|
||||
<main
|
||||
class="relative [.in-game_&]:hidden flex flex-col flex-1 overflow-hidden w-full px-[clamp(1.5rem,3vw,3rem)] pt-[clamp(0.75rem,1.5vw,1.5rem)] pb-[clamp(0.75rem,1.5vw,1.5rem)]"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-[20cm] mx-auto flex flex-col flex-1 gap-[clamp(1.5rem,3vw,3rem)] overflow-y-auto overflow-x-hidden [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden lg:[scrollbar-width:auto] lg:[-ms-overflow-style:auto] lg:[&::-webkit-scrollbar]:block"
|
||||
>
|
||||
<div
|
||||
id="page-play"
|
||||
class="flex flex-col gap-2 w-full max-w-6xl mx-auto px-0 sm:px-4 transition-all duration-300 my-auto min-h-0"
|
||||
>
|
||||
<token-login class="w-full hidden"></token-login>
|
||||
<main-layout class="contents">
|
||||
<play-page class="contents"></play-page>
|
||||
|
||||
<!-- Header / Identity Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-2 lg:gap-6 w-full">
|
||||
<div
|
||||
class="lg:col-span-9 flex flex-row flex-nowrap gap-x-2 h-[60px] items-center bg-slate-900/80 backdrop-blur-md p-3 rounded-xl border border-blue-500/20 relative z-20 text-sm sm:text-base shrink-0"
|
||||
>
|
||||
<!-- Flag -->
|
||||
<div
|
||||
class="h-[40px] sm:h-[50px] shrink-0 aspect-[4/3] flex items-center justify-center"
|
||||
>
|
||||
<!-- Hamburger (Mobile) -->
|
||||
<button
|
||||
id="hamburger-btn"
|
||||
class="lg:hidden flex w-full h-full bg-slate-800/40 text-white/90 border border-blue-400/20 hover:bg-slate-700/40 p-0 rounded-md items-center justify-center cursor-pointer transition-all duration-200"
|
||||
data-i18n-aria-label="main.menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="sidebar-menu"
|
||||
aria-haspopup="dialog"
|
||||
data-i18n-title="main.menu"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-8 h-8"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<news-modal
|
||||
id="page-news"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></news-modal>
|
||||
<single-player-modal
|
||||
id="page-single-player"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></single-player-modal>
|
||||
<host-lobby-modal
|
||||
id="page-host-lobby"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></host-lobby-modal>
|
||||
<join-private-lobby-modal
|
||||
id="page-join-private-lobby"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></join-private-lobby-modal>
|
||||
<territory-patterns-modal
|
||||
id="page-item-store"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></territory-patterns-modal>
|
||||
<matchmaking-modal
|
||||
id="page-matchmaking"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></matchmaking-modal>
|
||||
<user-setting
|
||||
id="page-settings"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></user-setting>
|
||||
<stats-modal
|
||||
id="page-stats"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></stats-modal>
|
||||
<account-modal
|
||||
id="page-account"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></account-modal>
|
||||
<help-modal
|
||||
id="page-help"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></help-modal>
|
||||
<language-modal
|
||||
id="page-language"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></language-modal>
|
||||
<flag-input-modal
|
||||
id="flag-input-modal"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></flag-input-modal>
|
||||
</main-layout>
|
||||
|
||||
<!-- Flag (Desktop) -->
|
||||
<flag-input
|
||||
id="flag-input-component"
|
||||
class="hidden lg:flex w-full h-full items-center justify-center rounded-md overflow-hidden"
|
||||
></flag-input>
|
||||
</div>
|
||||
<!-- Desktop Footer -->
|
||||
<page-footer></page-footer>
|
||||
|
||||
<!-- Username -->
|
||||
<div
|
||||
class="flex-1 min-w-0 h-[40px] sm:h-[50px] flex items-center"
|
||||
>
|
||||
<username-input
|
||||
class="relative w-full h-full block text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
></username-input>
|
||||
</div>
|
||||
|
||||
<!-- Pattern button (Mobile - inside bar, Desktop - hidden here) -->
|
||||
<button
|
||||
id="territory-patterns-input-preview-button"
|
||||
class="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"
|
||||
style="padding: 4px !important"
|
||||
data-i18n-title="main.pick_pattern"
|
||||
></button>
|
||||
</div>
|
||||
|
||||
<!-- Pattern button (Desktop only - separate column) -->
|
||||
<div class="hidden lg:flex lg:col-span-3">
|
||||
<div
|
||||
id="territory-patterns-preview-desktop-wrapper"
|
||||
class="w-full h-[60px]"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Game Actions Area -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 w-full">
|
||||
<!-- Left Column: Featured Lobbies / Quick Play -->
|
||||
<div class="lg:col-span-9 flex flex-col gap-6 min-w-0">
|
||||
<!-- Public Lobby Card -->
|
||||
<public-lobby
|
||||
class="block w-full transition-all duration-300"
|
||||
></public-lobby>
|
||||
|
||||
<!-- Matchmaking Buttons (Full Width within column) -->
|
||||
<!-- Not Logged In Button -->
|
||||
<button
|
||||
id="matchmaking-button-logged-out"
|
||||
class="w-full h-20 bg-purple-600/50 text-white font-black uppercase tracking-widest rounded-xl transition-all duration-200 flex flex-col items-center justify-center border border-purple-500/30 overflow-hidden relative opacity-60 cursor-not-allowed"
|
||||
disabled
|
||||
aria-disabled="true"
|
||||
>
|
||||
<span
|
||||
class="relative z-10 text-2xl"
|
||||
data-i18n="matchmaking_button.login_required"
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<!-- Logged In Button -->
|
||||
<button
|
||||
id="matchmaking-button"
|
||||
class="hidden w-full h-20 bg-purple-600 hover:bg-purple-500 text-white font-black uppercase tracking-widest rounded-xl transition-all duration-200 flex flex-col items-center justify-center border border-purple-500/30 group overflow-hidden relative"
|
||||
data-i18n-title="matchmaking_modal.title"
|
||||
>
|
||||
<span
|
||||
class="relative z-10 text-2xl"
|
||||
data-i18n="matchmaking_button.play_ranked"
|
||||
></span>
|
||||
<span
|
||||
class="relative z-10 text-xs font-medium text-purple-100 opacity-90 group-hover:opacity-100 transition-opacity"
|
||||
data-i18n="matchmaking_button.description"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Custom Games & Modes -->
|
||||
<div class="lg:col-span-3">
|
||||
<div class="h-40 lg:h-[30.5rem] relative z-10">
|
||||
<div
|
||||
class="h-full flex flex-col bg-slate-900/40 backdrop-blur-sm rounded-2xl border border-blue-400/10 overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="py-2 bg-blue-900/20 border-b border-blue-400/10 text-center text-sm font-bold text-gray-300 uppercase tracking-widest"
|
||||
data-i18n="host_modal.label"
|
||||
></div>
|
||||
<div class="flex-1 p-2 flex flex-row lg:flex-col gap-2">
|
||||
<o-button
|
||||
id="single-player"
|
||||
data-i18n-title="main.solo"
|
||||
translationKey="main.solo"
|
||||
fill
|
||||
class="flex-1 transition-transform hover:-translate-y-0.5"
|
||||
></o-button>
|
||||
|
||||
<o-button
|
||||
id="host-lobby-button"
|
||||
data-i18n-title="main.create"
|
||||
translationKey="main.create"
|
||||
fill
|
||||
secondary
|
||||
class="flex-1 opacity-90 hover:opacity-100"
|
||||
></o-button>
|
||||
|
||||
<o-button
|
||||
id="join-private-lobby-button"
|
||||
data-i18n-title="main.join"
|
||||
translationKey="main.join"
|
||||
fill
|
||||
secondary
|
||||
class="flex-1 opacity-90 hover:opacity-100"
|
||||
></o-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<news-modal
|
||||
id="page-news"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></news-modal>
|
||||
<single-player-modal
|
||||
id="page-single-player"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></single-player-modal>
|
||||
<host-lobby-modal
|
||||
id="page-host-lobby"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></host-lobby-modal>
|
||||
<join-private-lobby-modal
|
||||
id="page-join-private-lobby"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></join-private-lobby-modal>
|
||||
<territory-patterns-modal
|
||||
id="page-item-store"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></territory-patterns-modal>
|
||||
<matchmaking-modal
|
||||
id="page-matchmaking"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></matchmaking-modal>
|
||||
<user-setting
|
||||
id="page-options"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></user-setting>
|
||||
<keybinds-modal
|
||||
id="page-keybinds"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></keybinds-modal>
|
||||
<stats-modal
|
||||
id="page-stats"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></stats-modal>
|
||||
<account-modal
|
||||
id="page-account"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></account-modal>
|
||||
<help-modal
|
||||
id="page-help"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></help-modal>
|
||||
<language-modal
|
||||
id="page-language"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></language-modal>
|
||||
<flag-input-modal
|
||||
id="flag-input-modal"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></flag-input-modal>
|
||||
|
||||
<!-- Desktop Footer -->
|
||||
</div>
|
||||
</main>
|
||||
<footer
|
||||
class="[.in-game_&]:hidden bg-black/60 backdrop-blur-md flex flex-col items-center justify-center gap-2 pt-[2px] pb-2 text-white/50 w-full border-t border-white/10 shrink-0 mt-auto"
|
||||
>
|
||||
<div class="flex items-center justify-center gap-6 pt-2">
|
||||
<a
|
||||
href="https://github.com/openfrontio/OpenFrontIO"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="opacity-60 hover:opacity-100 hover:scale-110 transition-all"
|
||||
>
|
||||
<img
|
||||
src="/icons/github-mark-white.svg"
|
||||
data-i18n-alt="news.github_link"
|
||||
class="h-7 w-7 object-contain"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.reddit.com/r/OpenFrontIO/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="opacity-60 hover:opacity-100 hover:scale-110 transition-all"
|
||||
>
|
||||
<svg
|
||||
class="h-7 w-7 object-contain"
|
||||
viewBox="0 0 24 24"
|
||||
fill="white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.249-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://discord.gg/jRpxXvG42t"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="opacity-60 hover:opacity-100 hover:scale-110 transition-all"
|
||||
>
|
||||
<svg
|
||||
class="h-7 w-7 object-contain"
|
||||
viewBox="0 0 24 24"
|
||||
fill="white"
|
||||
>
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-14.36a.074.074 0 0 0-.032-.027zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.418 2.157-2.418 1.21 0 2.176 1.085 2.157 2.418 0 1.334-.956 2.419-2.157 2.419zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.418 2.157-2.418 1.21 0 2.176 1.085 2.157 2.418 0 1.334-.946 2.419-2.157 2.419z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://openfront.wiki/Main_Page"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="opacity-60 hover:opacity-100 hover:scale-110 transition-all"
|
||||
>
|
||||
<img
|
||||
src="/icons/wiki-logo.svg"
|
||||
data-i18n-alt="main.wiki"
|
||||
class="h-7 w-7 object-contain"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-xs mt-2 flex items-center justify-center gap-4">
|
||||
<a
|
||||
href="/terms-of-service.html"
|
||||
data-i18n="main.terms_of_service"
|
||||
target="_blank"
|
||||
class="hover:text-white transition-colors"
|
||||
></a>
|
||||
<span data-i18n="main.copyright"></span>
|
||||
<a
|
||||
href="/privacy-policy.html"
|
||||
data-i18n="main.privacy_policy"
|
||||
target="_blank"
|
||||
class="hover:text-white transition-colors"
|
||||
></a>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- Global Modals -->
|
||||
<territory-patterns-modal
|
||||
id="territory-patterns-modal"
|
||||
|
||||
@@ -26,12 +26,15 @@
|
||||
"title": "OpenFront (ALPHA)",
|
||||
"join_discord": "Discord",
|
||||
"login_discord": "Login with Discord",
|
||||
"sign_in": "Sign in",
|
||||
"discord_avatar_alt": "Discord profile avatar",
|
||||
"user_avatar_alt": "{username}'s avatar",
|
||||
"checking_login": "Checking login...",
|
||||
"logged_in": "Logged in!",
|
||||
"log_out": "Log out",
|
||||
"create": "Create Lobby",
|
||||
"join": "Join Lobby",
|
||||
"solo": "Solo Lobby",
|
||||
"solo": "Solo",
|
||||
"instructions": "Instructions",
|
||||
"game_info": "Game info",
|
||||
"wiki": "Wiki",
|
||||
@@ -42,7 +45,7 @@
|
||||
"play": "Play",
|
||||
"news": "News",
|
||||
"store": "Store",
|
||||
"options": "Options",
|
||||
"settings": "Settings",
|
||||
"keys": "Keys",
|
||||
"stats": "Stats",
|
||||
"account": "Account",
|
||||
@@ -179,13 +182,8 @@
|
||||
"title": "Account",
|
||||
"connected_as": "Connected as",
|
||||
"stats_overview": "Stats Overview",
|
||||
"save_progress_title": "Save Your Progress",
|
||||
"save_progress_desc": "Link your account to keep your stats, rank, and cosmetics safe.",
|
||||
"link_discord": "Link Discord Account",
|
||||
"link_via_email_placeholder": "Link via Email",
|
||||
"link_button": "Link",
|
||||
"log_out": "Log Out",
|
||||
"welcome_back": "Welcome Back",
|
||||
"sign_in_desc": "Sign in to save your stats and progress",
|
||||
"or": "OR",
|
||||
"email_placeholder": "Enter your email address",
|
||||
@@ -429,7 +427,7 @@
|
||||
"factory": "Factory"
|
||||
},
|
||||
"user_setting": {
|
||||
"title": "User Settings",
|
||||
"title": "Settings",
|
||||
"tab_basic": "Basic Settings",
|
||||
"tab_keybinds": "Keybinds",
|
||||
"dark_mode_label": "Dark Mode",
|
||||
|
||||
+4
-128
@@ -72,7 +72,7 @@ export class AccountModal extends BaseModal {
|
||||
const content = this.isLoadingUser
|
||||
? html`
|
||||
<div
|
||||
class="flex flex-col items-center justify-center p-12 text-white bg-black/40 backdrop-blur-md rounded-2xl border border-white/10 h-full min-h-[400px]"
|
||||
class="flex flex-col items-center justify-center p-12 text-white bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 h-full min-h-[400px]"
|
||||
>
|
||||
<div
|
||||
class="w-12 h-12 border-4 border-blue-500/30 border-t-blue-500 rounded-full animate-spin mb-4"
|
||||
@@ -107,7 +107,7 @@ export class AccountModal extends BaseModal {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="h-full flex flex-col bg-black/40 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden"
|
||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="flex items-center mb-6 pb-2 border-b border-white/10 gap-2 shrink-0 p-6"
|
||||
@@ -173,11 +173,7 @@ export class AccountModal extends BaseModal {
|
||||
const isLinked = me?.discord ?? me?.email;
|
||||
|
||||
if (!isLinked) {
|
||||
return html`
|
||||
<div class="p-6 flex justify-center items-start min-h-full">
|
||||
<div class="w-full max-w-2xl">${this.renderLinkAccountSection()}</div>
|
||||
</div>
|
||||
`;
|
||||
return this.renderLoginOptions();
|
||||
}
|
||||
|
||||
return html`
|
||||
@@ -235,60 +231,6 @@ export class AccountModal extends BaseModal {
|
||||
`;
|
||||
}
|
||||
|
||||
private renderLinkAccountSection(): TemplateResult {
|
||||
return html`
|
||||
<div class="bg-blue-500/10 rounded-xl border border-blue-500/20 p-6">
|
||||
<div class="flex items-start gap-4 mb-6">
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-white uppercase tracking-wider">
|
||||
${translateText("account_modal.save_progress_title")}
|
||||
</h3>
|
||||
<p class="text-sm text-white/60 mt-1">
|
||||
${translateText("account_modal.save_progress_desc")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
<button
|
||||
@click="${this.handleDiscordLogin}"
|
||||
class="w-full px-4 py-3 text-white bg-[#5865F2] hover:bg-[#4752C4] border border-transparent rounded-xl focus:outline-none transition-all duration-200 flex items-center justify-center gap-2 group relative overflow-hidden shadow-lg hover:shadow-xl"
|
||||
>
|
||||
<img
|
||||
src="/images/DiscordLogo.svg"
|
||||
alt="Discord"
|
||||
class="w-5 h-5 relative z-10"
|
||||
/>
|
||||
<span class="font-bold text-sm relative z-10 tracking-wide"
|
||||
>${translateText("main.login_discord") ||
|
||||
translateText("account_modal.link_discord")}</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<div class="relative group w-full">
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="email"
|
||||
.value="${this.email}"
|
||||
@input="${this.handleEmailInput}"
|
||||
class="flex-1 min-w-0 px-4 py-2 bg-black/40 border border-white/10 rounded-lg text-white text-sm placeholder-white/30 focus:outline-none focus:border-blue-500 transition-all font-medium"
|
||||
placeholder="${translateText(
|
||||
"account_modal.link_via_email_placeholder",
|
||||
)}"
|
||||
/>
|
||||
<button
|
||||
@click="${this.handleSubmit}"
|
||||
class="px-5 py-2 text-sm font-bold text-white uppercase bg-blue-600 hover:bg-blue-500 rounded-lg transition-all border border-blue-500/50 hover:shadow-[0_0_15px_rgba(37,99,235,0.3)] shrink-0"
|
||||
>
|
||||
${translateText("account_modal.link_button")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderLoggedInAs(): TemplateResult {
|
||||
const me = this.userMeResponse?.user;
|
||||
if (me?.discord) {
|
||||
@@ -309,46 +251,7 @@ export class AccountModal extends BaseModal {
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// "Mini" Login Options for linking account
|
||||
return html`
|
||||
<div class="w-full space-y-3">
|
||||
<button
|
||||
@click="${this.handleDiscordLogin}"
|
||||
class="w-full px-4 py-3 text-white bg-[#5865F2] hover:bg-[#4752C4] border border-transparent rounded-xl focus:outline-none transition-colors duration-200 flex items-center justify-center gap-2 group relative overflow-hidden"
|
||||
>
|
||||
<img
|
||||
src="/images/DiscordLogo.svg"
|
||||
alt="Discord"
|
||||
class="w-5 h-5 relative z-10"
|
||||
/>
|
||||
<span class="font-bold text-sm relative z-10 tracking-wide"
|
||||
>${translateText("main.login_discord") ||
|
||||
translateText("account_modal.link_discord")}</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<div class="relative group">
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="email"
|
||||
.value="${this.email}"
|
||||
@input="${this.handleEmailInput}"
|
||||
class="w-full px-4 py-2 bg-black/40 border border-white/10 rounded-lg text-white text-sm placeholder-white/30 focus:outline-none focus:border-blue-500 transition-all font-medium"
|
||||
placeholder="${translateText(
|
||||
"account_modal.link_via_email_placeholder",
|
||||
)}"
|
||||
/>
|
||||
<button
|
||||
@click="${this.handleSubmit}"
|
||||
class="px-4 py-2 text-sm font-bold text-white uppercase bg-blue-600 hover:bg-blue-500 rounded-lg transition-all"
|
||||
>
|
||||
${translateText("account_modal.link_button")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return html``;
|
||||
}
|
||||
|
||||
private viewGame(gameId: string): void {
|
||||
@@ -398,11 +301,6 @@ export class AccountModal extends BaseModal {
|
||||
<line x1="15" y1="12" x2="3" y2="12"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<h3
|
||||
class="text-xl font-bold text-white uppercase tracking-widest mb-2"
|
||||
>
|
||||
${translateText("account_modal.welcome_back")}
|
||||
</h3>
|
||||
<p class="text-white/50 text-sm font-medium">
|
||||
${translateText("account_modal.sign_in_desc")}
|
||||
</p>
|
||||
@@ -414,9 +312,6 @@ export class AccountModal extends BaseModal {
|
||||
@click="${this.handleDiscordLogin}"
|
||||
class="w-full px-6 py-4 text-white bg-[#5865F2] hover:bg-[#4752C4] border border-transparent rounded-xl focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#5865F2] transition-colors duration-200 flex items-center justify-center gap-3 group relative overflow-hidden shadow-lg hover:shadow-[#5865F2]/20"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-white/10 translate-y-full group-hover:translate-y-0 transition-transform duration-300"
|
||||
></div>
|
||||
<img
|
||||
src="/images/DiscordLogo.svg"
|
||||
alt="Discord"
|
||||
@@ -454,25 +349,6 @@ export class AccountModal extends BaseModal {
|
||||
)}"
|
||||
required
|
||||
/>
|
||||
<div
|
||||
class="absolute right-4 top-1/2 -translate-y-1/2 text-white/20"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="w-5 h-5"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"
|
||||
></path>
|
||||
<polyline points="22,6 12,13 2,6"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="${this.handleSubmit}"
|
||||
|
||||
+42
-8
@@ -1,5 +1,5 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { renderPlayerFlag } from "../core/CustomFlag";
|
||||
import { FlagSchema } from "../core/Schemas";
|
||||
import { translateText } from "./Utils";
|
||||
@@ -10,7 +10,12 @@ const flagKey: string = "flag";
|
||||
export class FlagInput extends LitElement {
|
||||
@state() public flag: string = "";
|
||||
|
||||
static styles = css``;
|
||||
@property({ type: Boolean, attribute: "show-select-label" })
|
||||
public showSelectLabel: boolean = false;
|
||||
|
||||
private isDefaultFlagValue(flag: string): boolean {
|
||||
return !flag || flag === "xx";
|
||||
}
|
||||
|
||||
public getCurrentFlag(): string {
|
||||
return this.flag;
|
||||
@@ -42,6 +47,17 @@ export class FlagInput extends LitElement {
|
||||
}
|
||||
};
|
||||
|
||||
private onInputClick(e: Event) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("flag-input-click", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.flag = this.getStoredFlag();
|
||||
@@ -59,18 +75,31 @@ export class FlagInput extends LitElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
const isDefaultFlag = this.isDefaultFlagValue(this.flag);
|
||||
const showSelect = this.showSelectLabel && isDefaultFlag;
|
||||
const buttonTitle = showSelect
|
||||
? translateText("flag_input.title")
|
||||
: translateText("flag_input.button_title");
|
||||
|
||||
return html`
|
||||
<button
|
||||
id="flag-input_"
|
||||
class="flag-btn m-0 border-0 bg-transparent hover:bg-white/10 w-full h-full flex cursor-pointer justify-center items-center focus:outline-none focus:ring-0 transition-colors duration-200"
|
||||
id="flag-input"
|
||||
class="flag-btn m-0 border-0 w-full h-full flex cursor-pointer justify-center items-center focus:outline-none focus:ring-0 transition-all duration-200 hover:scale-105 bg-slate-900/80 hover:bg-slate-800/80 active:bg-slate-800/90 rounded-lg overflow-hidden"
|
||||
style="padding: 0 !important;"
|
||||
title=${translateText("flag_input.button_title")}
|
||||
title=${buttonTitle}
|
||||
@click=${this.onInputClick}
|
||||
>
|
||||
<span
|
||||
id="flag-preview"
|
||||
class="w-full h-full overflow-hidden"
|
||||
style="display:block;"
|
||||
class=${showSelect ? "hidden" : "w-full h-full overflow-hidden"}
|
||||
></span>
|
||||
${showSelect
|
||||
? html`<span
|
||||
class="text-[10px] font-black text-white/40 uppercase leading-none break-words w-full text-center px-1"
|
||||
>
|
||||
${translateText("flag_input.title")}
|
||||
</span>`
|
||||
: null}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
@@ -81,6 +110,11 @@ export class FlagInput extends LitElement {
|
||||
) as HTMLElement;
|
||||
if (!preview) return;
|
||||
|
||||
if (this.showSelectLabel && this.isDefaultFlagValue(this.flag)) {
|
||||
preview.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
preview.innerHTML = "";
|
||||
|
||||
if (this.flag?.startsWith("!")) {
|
||||
|
||||
@@ -18,7 +18,7 @@ export class FlagInputModal extends BaseModal {
|
||||
render() {
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full flex flex-col bg-black/40 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden"
|
||||
class="h-full flex flex-col bg-black/60 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"
|
||||
@@ -54,7 +54,7 @@ export class FlagInputModal extends BaseModal {
|
||||
|
||||
<div class="flex justify-center w-full px-6 pb-4 shrink-0">
|
||||
<input
|
||||
class="h-12 w-full max-w-md border border-white/10 bg-black/40
|
||||
class="h-12 w-full max-w-md border border-white/10 bg-black/60
|
||||
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"
|
||||
|
||||
@@ -98,7 +98,7 @@ export class HelpModal extends BaseModal {
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full flex flex-col ${this.inline
|
||||
? "bg-black/40 backdrop-blur-md rounded-2xl border border-white/10 p-6"
|
||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 p-6"
|
||||
: ""}"
|
||||
>
|
||||
<div class="flex items-center mb-6 pb-2 border-b border-white/10 gap-2">
|
||||
|
||||
@@ -102,7 +102,7 @@ export class HostLobbyModal extends BaseModal {
|
||||
render() {
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full flex flex-col bg-black/40 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden select-none"
|
||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden select-none"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
@@ -202,6 +202,25 @@ export class HostLobbyModal extends BaseModal {
|
||||
? this.lobbyId
|
||||
: "••••••••"}
|
||||
</button>
|
||||
<button
|
||||
@click=${this.copyToClipboard}
|
||||
class="p-1.5 rounded-md hover:bg-white/10 text-white/60 hover:text-white transition-colors"
|
||||
title="${translateText("common.click_to_copy")}"
|
||||
aria-label="${translateText("common.click_to_copy")}"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
height="16px"
|
||||
width="16px"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -614,7 +633,7 @@ export class HostLobbyModal extends BaseModal {
|
||||
min="0"
|
||||
max="120"
|
||||
.value=${String(this.maxTimerValue ?? 0)}
|
||||
class="w-full text-center rounded bg-black/40 text-white text-sm font-bold border border-white/20 focus:outline-none focus:border-blue-500 p-1 my-1"
|
||||
class="w-full text-center rounded bg-black/60 text-white text-sm font-bold border border-white/20 focus:outline-none focus:border-blue-500 p-1 my-1"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
@input=${this.handleMaxTimerValueChanges}
|
||||
@keydown=${this.handleMaxTimerValueKeyDown}
|
||||
@@ -694,7 +713,7 @@ export class HostLobbyModal extends BaseModal {
|
||||
.value=${String(
|
||||
this.spawnImmunityDurationMinutes ?? 0,
|
||||
)}
|
||||
class="w-full text-center rounded bg-black/40 text-white text-sm font-bold border border-white/20 focus:outline-none focus:border-blue-500 p-1 my-1"
|
||||
class="w-full text-center rounded bg-black/60 text-white text-sm font-bold border border-white/20 focus:outline-none focus:border-blue-500 p-1 my-1"
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
@input=${this.handleSpawnImmunityDurationInput}
|
||||
@keydown=${this.handleSpawnImmunityDurationKeyDown}
|
||||
|
||||
@@ -38,7 +38,7 @@ export class JoinPrivateLobbyModal extends BaseModal {
|
||||
render() {
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full flex flex-col bg-black/40 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden select-none"
|
||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden select-none"
|
||||
>
|
||||
<div
|
||||
class="flex items-center mb-6 pb-2 border-b border-white/10 gap-2 shrink-0 p-6"
|
||||
@@ -137,6 +137,25 @@ export class JoinPrivateLobbyModal extends BaseModal {
|
||||
? this.currentLobbyId
|
||||
: "••••••••"}
|
||||
</div>
|
||||
<button
|
||||
@click=${this.copyToClipboard}
|
||||
class="p-1.5 rounded-md hover:bg-white/10 text-white/60 hover:text-white transition-colors"
|
||||
title="${translateText("common.click_to_copy")}"
|
||||
aria-label="${translateText("common.click_to_copy")}"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
height="16px"
|
||||
width="16px"
|
||||
fill="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
d="M16 1H4c-1.1 0-2 .9-2 2v12h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>`
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
@@ -1,522 +0,0 @@
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { formatKeyForDisplay, translateText } from "../client/Utils";
|
||||
import "./components/baseComponents/setting/SettingKeybind";
|
||||
import { SettingKeybind } from "./components/baseComponents/setting/SettingKeybind";
|
||||
import { BaseModal } from "./components/BaseModal";
|
||||
|
||||
const DefaultKeybinds: Record<string, string> = {
|
||||
toggleView: "Space",
|
||||
buildCity: "Digit1",
|
||||
buildFactory: "Digit2",
|
||||
buildPort: "Digit3",
|
||||
buildDefensePost: "Digit4",
|
||||
buildMissileSilo: "Digit5",
|
||||
buildSamLauncher: "Digit6",
|
||||
buildWarship: "Digit7",
|
||||
buildAtomBomb: "Digit8",
|
||||
buildHydrogenBomb: "Digit9",
|
||||
buildMIRV: "Digit0",
|
||||
attackRatioDown: "KeyT",
|
||||
attackRatioUp: "KeyY",
|
||||
boatAttack: "KeyB",
|
||||
groundAttack: "KeyG",
|
||||
zoomOut: "KeyQ",
|
||||
zoomIn: "KeyE",
|
||||
centerCamera: "KeyC",
|
||||
moveUp: "KeyW",
|
||||
moveLeft: "KeyA",
|
||||
moveDown: "KeyS",
|
||||
moveRight: "KeyD",
|
||||
};
|
||||
|
||||
@customElement("keybinds-modal")
|
||||
export class KeybindsModal extends BaseModal {
|
||||
@state() private keybinds: Record<
|
||||
string,
|
||||
{ value: string | string[]; key: string }
|
||||
> = {};
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
const savedKeybinds = localStorage.getItem("settings.keybinds");
|
||||
if (savedKeybinds) {
|
||||
try {
|
||||
const parsed = JSON.parse(savedKeybinds);
|
||||
// Validate shape: ensure all values have 'value' and 'key' properties with correct types
|
||||
if (
|
||||
typeof parsed === "object" &&
|
||||
parsed !== null &&
|
||||
!Array.isArray(parsed)
|
||||
) {
|
||||
const isValid = Object.values(parsed).every((entry) => {
|
||||
// Ensure entry is an object (not null, not array, not primitive)
|
||||
if (
|
||||
typeof entry !== "object" ||
|
||||
entry === null ||
|
||||
Array.isArray(entry)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// Ensure 'key' property exists and is a string
|
||||
if (!("key" in entry) || typeof entry.key !== "string") {
|
||||
return false;
|
||||
}
|
||||
// Ensure 'value' property exists and is either a string or an array of strings
|
||||
if (!("value" in entry)) {
|
||||
return false;
|
||||
}
|
||||
if (typeof entry.value === "string") {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(entry.value)) {
|
||||
return entry.value.every((v) => typeof v === "string");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (isValid) {
|
||||
this.keybinds = parsed;
|
||||
} else {
|
||||
console.warn(
|
||||
"Invalid keybinds structure: entries must be objects with 'key' (string) and 'value' (string or string[]) properties. Ignoring saved data.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Invalid keybinds data: expected non-array object. Ignoring saved data.",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Invalid keybinds JSON:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeybindChange(
|
||||
e: CustomEvent<{
|
||||
action: string;
|
||||
value: string;
|
||||
key: string;
|
||||
prevValue?: string;
|
||||
}>,
|
||||
) {
|
||||
const { action, value, key, prevValue } = e.detail;
|
||||
|
||||
const activeKeybinds: Record<string, string> = { ...DefaultKeybinds };
|
||||
for (const [k, v] of Object.entries(this.keybinds)) {
|
||||
// Normalize value to string
|
||||
const normalizedValue = Array.isArray(v.value)
|
||||
? v.value[0] || ""
|
||||
: v.value;
|
||||
if (normalizedValue === "Null") {
|
||||
delete activeKeybinds[k];
|
||||
} else {
|
||||
activeKeybinds[k] = normalizedValue;
|
||||
}
|
||||
}
|
||||
|
||||
const values = Object.entries(activeKeybinds)
|
||||
.filter(([k]) => k !== action)
|
||||
.map(([, v]) => v);
|
||||
|
||||
if (values.includes(value) && value !== "Null") {
|
||||
// Format key for user-friendly display
|
||||
const displayKey = formatKeyForDisplay(key || value);
|
||||
// Use heads-up-message modal for error popup
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show-message", {
|
||||
detail: {
|
||||
message: html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-red-500 inline-block align-middle mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium">
|
||||
${(() => {
|
||||
const message = translateText(
|
||||
"user_setting.keybind_conflict_error",
|
||||
{ key: displayKey },
|
||||
);
|
||||
const parts = message.split(displayKey);
|
||||
return html`${parts[0]}<span
|
||||
class="font-mono font-bold bg-white/10 px-1.5 py-0.5 rounded text-red-200 mx-1 border border-white/10"
|
||||
>${displayKey}</span
|
||||
>${parts[1] || ""}`;
|
||||
})()}
|
||||
</span>
|
||||
`,
|
||||
color: "red",
|
||||
duration: 3000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const element = this.renderRoot.querySelector(
|
||||
`setting-keybind[action="${action}"]`,
|
||||
) as SettingKeybind;
|
||||
if (element) {
|
||||
// Restore the previous value, or use default keybind if no previous override
|
||||
element.value = prevValue ?? DefaultKeybinds[action] ?? "";
|
||||
element.requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.keybinds = { ...this.keybinds, [action]: { value: value, key: key } };
|
||||
localStorage.setItem("settings.keybinds", JSON.stringify(this.keybinds));
|
||||
}
|
||||
|
||||
private getKeyValue(action: string): string | undefined {
|
||||
const entry = this.keybinds[action];
|
||||
if (!entry) return undefined;
|
||||
// Normalize value to string
|
||||
const normalizedValue = Array.isArray(entry.value)
|
||||
? entry.value[0] || ""
|
||||
: entry.value;
|
||||
if (normalizedValue === "Null") return "";
|
||||
return normalizedValue || undefined;
|
||||
}
|
||||
|
||||
private getKeyChar(action: string): string {
|
||||
const entry = this.keybinds[action];
|
||||
if (!entry) return "";
|
||||
return entry.key || "";
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full flex flex-col ${this.inline
|
||||
? "bg-black/40 backdrop-blur-md rounded-2xl border border-white/10"
|
||||
: ""}"
|
||||
>
|
||||
<div
|
||||
class="flex items-center mb-6 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("user_setting.tab_keybinds")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent px-6 pb-6 mr-1"
|
||||
>
|
||||
<div class="flex flex-col gap-2">${this.renderKeybindSettings()}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (this.inline) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return html`
|
||||
<o-modal
|
||||
title="${translateText("user_setting.tab_keybinds")}"
|
||||
?inline=${this.inline}
|
||||
hideCloseButton
|
||||
hideHeader
|
||||
>
|
||||
${content}
|
||||
</o-modal>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderKeybindSettings() {
|
||||
return html`
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-4 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.view_options")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="toggleView"
|
||||
label=${translateText("user_setting.toggle_view")}
|
||||
description=${translateText("user_setting.toggle_view_desc")}
|
||||
defaultKey="Space"
|
||||
.value=${this.getKeyValue("toggleView")}
|
||||
.display=${this.getKeyChar("toggleView")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.build_controls")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="buildCity"
|
||||
label=${translateText("user_setting.build_city")}
|
||||
description=${translateText("user_setting.build_city_desc")}
|
||||
defaultKey="Digit1"
|
||||
.value=${this.getKeyValue("buildCity")}
|
||||
.display=${this.getKeyChar("buildCity")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildFactory"
|
||||
label=${translateText("user_setting.build_factory")}
|
||||
description=${translateText("user_setting.build_factory_desc")}
|
||||
defaultKey="Digit2"
|
||||
.value=${this.getKeyValue("buildFactory")}
|
||||
.display=${this.getKeyChar("buildFactory")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildPort"
|
||||
label=${translateText("user_setting.build_port")}
|
||||
description=${translateText("user_setting.build_port_desc")}
|
||||
defaultKey="Digit3"
|
||||
.value=${this.getKeyValue("buildPort")}
|
||||
.display=${this.getKeyChar("buildPort")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildDefensePost"
|
||||
label=${translateText("user_setting.build_defense_post")}
|
||||
description=${translateText("user_setting.build_defense_post_desc")}
|
||||
defaultKey="Digit4"
|
||||
.value=${this.getKeyValue("buildDefensePost")}
|
||||
.display=${this.getKeyChar("buildDefensePost")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildMissileSilo"
|
||||
label=${translateText("user_setting.build_missile_silo")}
|
||||
description=${translateText("user_setting.build_missile_silo_desc")}
|
||||
defaultKey="Digit5"
|
||||
.value=${this.getKeyValue("buildMissileSilo")}
|
||||
.display=${this.getKeyChar("buildMissileSilo")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildSamLauncher"
|
||||
label=${translateText("user_setting.build_sam_launcher")}
|
||||
description=${translateText("user_setting.build_sam_launcher_desc")}
|
||||
defaultKey="Digit6"
|
||||
.value=${this.getKeyValue("buildSamLauncher")}
|
||||
.display=${this.getKeyChar("buildSamLauncher")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildWarship"
|
||||
label=${translateText("user_setting.build_warship")}
|
||||
description=${translateText("user_setting.build_warship_desc")}
|
||||
defaultKey="Digit7"
|
||||
.value=${this.getKeyValue("buildWarship")}
|
||||
.display=${this.getKeyChar("buildWarship")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildAtomBomb"
|
||||
label=${translateText("user_setting.build_atom_bomb")}
|
||||
description=${translateText("user_setting.build_atom_bomb_desc")}
|
||||
defaultKey="Digit8"
|
||||
.value=${this.getKeyValue("buildAtomBomb")}
|
||||
.display=${this.getKeyChar("buildAtomBomb")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildHydrogenBomb"
|
||||
label=${translateText("user_setting.build_hydrogen_bomb")}
|
||||
description=${translateText("user_setting.build_hydrogen_bomb_desc")}
|
||||
defaultKey="Digit9"
|
||||
.value=${this.getKeyValue("buildHydrogenBomb")}
|
||||
.display=${this.getKeyChar("buildHydrogenBomb")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildMIRV"
|
||||
label=${translateText("user_setting.build_mirv")}
|
||||
description=${translateText("user_setting.build_mirv_desc")}
|
||||
defaultKey="Digit0"
|
||||
.value=${this.getKeyValue("buildMIRV")}
|
||||
.display=${this.getKeyChar("buildMIRV")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.attack_ratio_controls")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="attackRatioDown"
|
||||
label=${translateText("user_setting.attack_ratio_down")}
|
||||
description=${translateText("user_setting.attack_ratio_down_desc")}
|
||||
defaultKey="KeyT"
|
||||
.value=${this.getKeyValue("attackRatioDown")}
|
||||
.display=${this.getKeyChar("attackRatioDown")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="attackRatioUp"
|
||||
label=${translateText("user_setting.attack_ratio_up")}
|
||||
description=${translateText("user_setting.attack_ratio_up_desc")}
|
||||
defaultKey="KeyY"
|
||||
.value=${this.getKeyValue("attackRatioUp")}
|
||||
.display=${this.getKeyChar("attackRatioUp")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.attack_keybinds")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="boatAttack"
|
||||
label=${translateText("user_setting.boat_attack")}
|
||||
description=${translateText("user_setting.boat_attack_desc")}
|
||||
defaultKey="KeyB"
|
||||
.value=${this.getKeyValue("boatAttack")}
|
||||
.display=${this.getKeyChar("boatAttack")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="groundAttack"
|
||||
label=${translateText("user_setting.ground_attack")}
|
||||
description=${translateText("user_setting.ground_attack_desc")}
|
||||
defaultKey="KeyG"
|
||||
.value=${this.getKeyValue("groundAttack")}
|
||||
.display=${this.getKeyChar("groundAttack")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.zoom_controls")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="zoomOut"
|
||||
label=${translateText("user_setting.zoom_out")}
|
||||
description=${translateText("user_setting.zoom_out_desc")}
|
||||
defaultKey="KeyQ"
|
||||
.value=${this.getKeyValue("zoomOut")}
|
||||
.display=${this.getKeyChar("zoomOut")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="zoomIn"
|
||||
label=${translateText("user_setting.zoom_in")}
|
||||
description=${translateText("user_setting.zoom_in_desc")}
|
||||
defaultKey="KeyE"
|
||||
.value=${this.getKeyValue("zoomIn")}
|
||||
.display=${this.getKeyChar("zoomIn")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.camera_movement")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="centerCamera"
|
||||
label=${translateText("user_setting.center_camera")}
|
||||
description=${translateText("user_setting.center_camera_desc")}
|
||||
defaultKey="KeyC"
|
||||
.value=${this.getKeyValue("centerCamera")}
|
||||
.display=${this.getKeyChar("centerCamera")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveUp"
|
||||
label=${translateText("user_setting.move_up")}
|
||||
description=${translateText("user_setting.move_up_desc")}
|
||||
defaultKey="KeyW"
|
||||
.value=${this.getKeyValue("moveUp")}
|
||||
.display=${this.getKeyChar("moveUp")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveLeft"
|
||||
label=${translateText("user_setting.move_left")}
|
||||
description=${translateText("user_setting.move_left_desc")}
|
||||
defaultKey="KeyA"
|
||||
.value=${this.getKeyValue("moveLeft")}
|
||||
.display=${this.getKeyChar("moveLeft")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveDown"
|
||||
label=${translateText("user_setting.move_down")}
|
||||
description=${translateText("user_setting.move_down_desc")}
|
||||
defaultKey="KeyS"
|
||||
.value=${this.getKeyValue("moveDown")}
|
||||
.display=${this.getKeyChar("moveDown")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveRight"
|
||||
label=${translateText("user_setting.move_right")}
|
||||
description=${translateText("user_setting.move_right_desc")}
|
||||
defaultKey="KeyD"
|
||||
.value=${this.getKeyValue("moveRight")}
|
||||
.display=${this.getKeyChar("moveRight")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
`;
|
||||
}
|
||||
|
||||
protected onOpen(): void {
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
@@ -220,11 +220,11 @@ export class LangSelector extends LitElement {
|
||||
"o-modal",
|
||||
"o-button",
|
||||
"territory-patterns-modal",
|
||||
"pattern-input",
|
||||
"fluent-slider",
|
||||
"news-modal",
|
||||
"news-button",
|
||||
"account-modal",
|
||||
"keybinds-modal",
|
||||
"stats-modal",
|
||||
"flag-input-modal",
|
||||
"flag-input",
|
||||
@@ -243,6 +243,27 @@ export class LangSelector extends LitElement {
|
||||
element.textContent = text;
|
||||
});
|
||||
|
||||
const applyAttributeTranslation = (
|
||||
dataAttr: string,
|
||||
targetAttr: string,
|
||||
): void => {
|
||||
document.querySelectorAll(`[${dataAttr}]`).forEach((element) => {
|
||||
const key = element.getAttribute(dataAttr);
|
||||
if (key === null) return;
|
||||
const text = this.translateText(key);
|
||||
if (text === null) {
|
||||
console.warn(`Translation key not found: ${key}`);
|
||||
return;
|
||||
}
|
||||
element.setAttribute(targetAttr, text);
|
||||
});
|
||||
};
|
||||
|
||||
applyAttributeTranslation("data-i18n-title", "title");
|
||||
applyAttributeTranslation("data-i18n-alt", "alt");
|
||||
applyAttributeTranslation("data-i18n-aria-label", "aria-label");
|
||||
applyAttributeTranslation("data-i18n-placeholder", "placeholder");
|
||||
|
||||
components.forEach((tag) => {
|
||||
document.querySelectorAll(tag).forEach((el) => {
|
||||
if (typeof (el as any).requestUpdate === "function") {
|
||||
|
||||
@@ -32,7 +32,7 @@ export class LanguageModal extends BaseModal {
|
||||
<div
|
||||
class="h-full flex flex-col ${
|
||||
this.inline
|
||||
? "bg-black/40 backdrop-blur-md rounded-2xl border border-white/10 p-6"
|
||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 p-6"
|
||||
: "bg-[#232323] text-white"
|
||||
}"
|
||||
>
|
||||
|
||||
+73
-70
@@ -1,81 +1,84 @@
|
||||
export function initLayout() {
|
||||
const hb = document.getElementById("hamburger-btn");
|
||||
const sidebar = document.getElementById("sidebar-menu");
|
||||
const backdrop = document.getElementById("mobile-menu-backdrop");
|
||||
// Wait for play-page component to render before setting up hamburger menu
|
||||
customElements.whenDefined("play-page").then(() => {
|
||||
const hb = document.getElementById("hamburger-btn");
|
||||
const sidebar = document.getElementById("sidebar-menu");
|
||||
const backdrop = document.getElementById("mobile-menu-backdrop");
|
||||
|
||||
// Force sidebar visibility style to ensure it's not hidden by other CSS
|
||||
if (sidebar && window.innerWidth < 768) {
|
||||
sidebar.style.display = "flex";
|
||||
}
|
||||
|
||||
if (!hb) {
|
||||
console.error("Hamburger button not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Disable fallback inline handler now that JS is loaded
|
||||
hb.onclick = null;
|
||||
|
||||
if (!sidebar) {
|
||||
console.error("Sidebar menu not found");
|
||||
return;
|
||||
}
|
||||
if (!backdrop) {
|
||||
console.error("Mobile menu backdrop not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const setMenuState = (open: boolean) => {
|
||||
sidebar.classList.toggle("open", open);
|
||||
backdrop.classList.toggle("open", open);
|
||||
document.documentElement.classList.toggle("overflow-hidden", open);
|
||||
hb.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
};
|
||||
|
||||
const closeMenu = () => setMenuState(false);
|
||||
const openMenu = () => setMenuState(true);
|
||||
|
||||
const toggle = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
// Only prevent default if it's a touchstart to avoid ghost clicks
|
||||
if ((e as any).type === "touchstart") {
|
||||
(e as Event).preventDefault();
|
||||
// Force sidebar visibility style to ensure it's not hidden by other CSS
|
||||
if (sidebar && window.innerWidth < 768) {
|
||||
sidebar.style.display = "flex";
|
||||
}
|
||||
|
||||
const opening = !sidebar.classList.contains("open");
|
||||
if (opening) {
|
||||
openMenu();
|
||||
} else {
|
||||
closeMenu();
|
||||
if (!hb) {
|
||||
console.error("Hamburger button not found");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
hb.addEventListener("click", toggle);
|
||||
// Disable fallback inline handler now that JS is loaded
|
||||
hb.onclick = null;
|
||||
|
||||
backdrop.addEventListener("click", closeMenu);
|
||||
|
||||
// Close menu when clicking a menu link or button (Mobile only)
|
||||
sidebar.addEventListener("click", (e) => {
|
||||
// On desktop, we want the menu to stay open unless explicitly toggled
|
||||
if (window.innerWidth >= 768) return;
|
||||
|
||||
// If the click happened on or inside an anchor/button/menu item, close the menu
|
||||
const clickedElement = (e.target as Element).closest
|
||||
? (e.target as Element).closest(
|
||||
'a, button, [role="menuitem"], .nav-menu-item',
|
||||
)
|
||||
: null;
|
||||
|
||||
if (clickedElement) {
|
||||
closeMenu();
|
||||
if (!sidebar) {
|
||||
console.error("Sidebar menu not found");
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Close on Escape (Mobile only)
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (window.innerWidth >= 768) return;
|
||||
if (e.key === "Escape" && sidebar.classList.contains("open")) {
|
||||
closeMenu();
|
||||
if (!backdrop) {
|
||||
console.error("Mobile menu backdrop not found");
|
||||
return;
|
||||
}
|
||||
|
||||
const setMenuState = (open: boolean) => {
|
||||
sidebar.classList.toggle("open", open);
|
||||
backdrop.classList.toggle("open", open);
|
||||
document.documentElement.classList.toggle("overflow-hidden", open);
|
||||
hb.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
};
|
||||
|
||||
const closeMenu = () => setMenuState(false);
|
||||
const openMenu = () => setMenuState(true);
|
||||
|
||||
const toggle = (e: Event) => {
|
||||
e.stopPropagation();
|
||||
// Only prevent default if it's a touchstart to avoid ghost clicks
|
||||
if ((e as any).type === "touchstart") {
|
||||
(e as Event).preventDefault();
|
||||
}
|
||||
|
||||
const opening = !sidebar.classList.contains("open");
|
||||
if (opening) {
|
||||
openMenu();
|
||||
} else {
|
||||
closeMenu();
|
||||
}
|
||||
};
|
||||
|
||||
hb.addEventListener("click", toggle);
|
||||
|
||||
backdrop.addEventListener("click", closeMenu);
|
||||
|
||||
// Close menu when clicking a menu link or button (Mobile only)
|
||||
sidebar.addEventListener("click", (e) => {
|
||||
// On desktop, we want the menu to stay open unless explicitly toggled
|
||||
if (window.innerWidth >= 768) return;
|
||||
|
||||
// If the click happened on or inside an anchor/button/menu item, close the menu
|
||||
const clickedElement = (e.target as Element).closest
|
||||
? (e.target as Element).closest(
|
||||
'a, button, [role="menuitem"], .nav-menu-item',
|
||||
)
|
||||
: null;
|
||||
|
||||
if (clickedElement) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
|
||||
// Close on Escape (Mobile only)
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (window.innerWidth >= 768) return;
|
||||
if (e.key === "Escape" && sidebar.classList.contains("open")) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+150
-78
@@ -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,
|
||||
|
||||
@@ -48,7 +48,7 @@ export class MatchmakingModal extends BaseModal {
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full flex flex-col ${this.inline
|
||||
? "bg-black/40 backdrop-blur-md rounded-2xl border border-white/10"
|
||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10"
|
||||
: ""}"
|
||||
>
|
||||
<div
|
||||
|
||||
+83
-46
@@ -1,26 +1,39 @@
|
||||
export function initNavigation() {
|
||||
const showPage = (pageId: string) => {
|
||||
// Hide all pages
|
||||
document.querySelectorAll(".page-content").forEach((el) => {
|
||||
el.classList.add("hidden");
|
||||
el.classList.remove("block");
|
||||
});
|
||||
document.getElementById("page-play")?.classList.add("hidden");
|
||||
(window as any).currentPageId = pageId;
|
||||
|
||||
const target = document.getElementById(pageId);
|
||||
if (target) {
|
||||
target.classList.remove("hidden");
|
||||
// Modals need block display explicitly
|
||||
if (target.classList.contains("page-content")) {
|
||||
target.classList.add("block");
|
||||
}
|
||||
// Hide only the currently visible modal
|
||||
const visibleModal = document.querySelector(".page-content:not(.hidden)");
|
||||
if (visibleModal) {
|
||||
visibleModal.classList.add("hidden");
|
||||
visibleModal.classList.remove("block");
|
||||
}
|
||||
|
||||
// If the target itself is a modal component with inline attribute, open it
|
||||
if (
|
||||
target.hasAttribute("inline") &&
|
||||
typeof (target as any).open === "function"
|
||||
) {
|
||||
(target as any).open();
|
||||
// Handle page-play separately (it's not a page-content element)
|
||||
const pagePlayEl = document.getElementById("page-play");
|
||||
if (pageId === "page-play") {
|
||||
pagePlayEl?.classList.remove("hidden");
|
||||
} else {
|
||||
pagePlayEl?.classList.add("hidden");
|
||||
}
|
||||
|
||||
// Show the target page if it's a modal
|
||||
if (pageId !== "page-play") {
|
||||
const target = document.getElementById(pageId);
|
||||
if (target) {
|
||||
target.classList.remove("hidden");
|
||||
// Modals need block display explicitly
|
||||
if (target.classList.contains("page-content")) {
|
||||
target.classList.add("block");
|
||||
}
|
||||
|
||||
// If the target itself is a modal component with inline attribute, open it
|
||||
if (
|
||||
target.hasAttribute("inline") &&
|
||||
typeof (target as any).open === "function"
|
||||
) {
|
||||
(target as any).open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,39 +52,63 @@ export function initNavigation() {
|
||||
|
||||
window.showPage = showPage;
|
||||
|
||||
document.querySelectorAll(".nav-menu-item[data-page]").forEach((el) => {
|
||||
el.addEventListener("click", () => {
|
||||
const pageId = (el as HTMLElement).dataset.page;
|
||||
// Use event delegation for navigation items (they may be inside Lit components)
|
||||
document.addEventListener("click", (e) => {
|
||||
const target = (e.target as HTMLElement).closest(
|
||||
".nav-menu-item[data-page]",
|
||||
);
|
||||
if (target) {
|
||||
const pageId = (target as HTMLElement).dataset.page;
|
||||
if (pageId) showPage(pageId);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Handle clicks on main container to close open modals (navigate back)
|
||||
const mainEl = document.querySelector("main");
|
||||
if (mainEl) {
|
||||
mainEl.addEventListener("click", (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const isPlayPageHidden = document
|
||||
.getElementById("page-play")
|
||||
?.classList.contains("hidden");
|
||||
// Wait for main-layout component to render before setting up click handler
|
||||
customElements.whenDefined("main-layout").then(() => {
|
||||
// Handle clicks on main container to close open modals (navigate back)
|
||||
const mainEl = document.querySelector("main");
|
||||
|
||||
// Only proceed if we are NOT on the play page (meaning a modal page is open)
|
||||
if (isPlayPageHidden) {
|
||||
// If clicking on the main container directly (e.g. padding/background)
|
||||
// or the max-width wrapper div directly
|
||||
const wrapper = mainEl.firstElementChild as HTMLElement;
|
||||
if (target === mainEl || (wrapper && target === wrapper)) {
|
||||
showPage("page-play");
|
||||
if (mainEl) {
|
||||
mainEl.addEventListener("click", (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
const isPlayPageHidden = document
|
||||
.getElementById("page-play")
|
||||
?.classList.contains("hidden");
|
||||
|
||||
// Only proceed if we are NOT on the play page (meaning a modal page is open)
|
||||
if (isPlayPageHidden) {
|
||||
// Close modal if clicking on main element itself, or directly on a page-content element
|
||||
const isOnMain = target === mainEl;
|
||||
const isOnPageContent = target.classList.contains("page-content");
|
||||
|
||||
if (isOnMain || isOnPageContent) {
|
||||
// Find the open modal and call its close() method instead of showPage directly
|
||||
// This ensures proper cleanup (like websocket disconnection)
|
||||
const openModal = document.querySelector(
|
||||
".page-content:not(.hidden)",
|
||||
) as any;
|
||||
|
||||
if (openModal && typeof openModal.close === "function") {
|
||||
// Call leaveLobby or closeAndLeave first if it exists (for lobby modals)
|
||||
if (typeof openModal.leaveLobby === "function") {
|
||||
openModal.leaveLobby();
|
||||
} else if (typeof openModal.closeAndLeave === "function") {
|
||||
openModal.closeAndLeave();
|
||||
return; // closeAndLeave already calls close()
|
||||
}
|
||||
openModal.close();
|
||||
} else {
|
||||
showPage("page-play");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Set default active if not set
|
||||
const initialPage = document.querySelector(
|
||||
'.nav-menu-item[data-page="page-play"]',
|
||||
);
|
||||
if (initialPage && !initialPage.classList.contains("active")) {
|
||||
// Set default page to play if no menu item is active
|
||||
const anyActive = document.querySelector(".nav-menu-item.active");
|
||||
if (!anyActive) {
|
||||
showPage("page-play");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export class NewsModal extends BaseModal {
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full flex flex-col ${this.inline
|
||||
? "bg-black/40 backdrop-blur-md rounded-2xl border border-white/10"
|
||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10"
|
||||
: ""}"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import { Cosmetics } from "../core/CosmeticSchemas";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import { PlayerPattern } from "../core/Schemas";
|
||||
import { renderPatternPreview } from "./components/PatternButton";
|
||||
import { fetchCosmetics } from "./Cosmetics";
|
||||
import { translateText } from "./Utils";
|
||||
|
||||
// Module-level cosmetics cache to avoid refetching on every component mount
|
||||
let cosmeticsCache: Promise<Cosmetics | null> | null = null;
|
||||
|
||||
function getCachedCosmetics(): Promise<Cosmetics | null> {
|
||||
if (!cosmeticsCache) {
|
||||
const fetchPromise = fetchCosmetics();
|
||||
cosmeticsCache = fetchPromise.catch((err) => {
|
||||
cosmeticsCache = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
return cosmeticsCache;
|
||||
}
|
||||
|
||||
@customElement("pattern-input")
|
||||
export class PatternInput extends LitElement {
|
||||
@state() public pattern: PlayerPattern | null = null;
|
||||
@state() public selectedColor: string | null = null;
|
||||
@state() private isLoading: boolean = true;
|
||||
|
||||
@property({ type: Boolean, attribute: "show-select-label" })
|
||||
public showSelectLabel: boolean = false;
|
||||
|
||||
private userSettings = new UserSettings();
|
||||
private cosmetics: Cosmetics | null = null;
|
||||
private _abortController: AbortController | null = null;
|
||||
|
||||
private _onPatternSelected = () => {
|
||||
this.updateFromSettings();
|
||||
};
|
||||
|
||||
private updateFromSettings() {
|
||||
this.selectedColor = this.userSettings.getSelectedColor() ?? null;
|
||||
|
||||
if (this.cosmetics) {
|
||||
this.pattern = this.userSettings.getSelectedPatternName(this.cosmetics);
|
||||
} else {
|
||||
this.pattern = null;
|
||||
}
|
||||
}
|
||||
|
||||
private onInputClick(e: Event) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("pattern-input-click", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._abortController = new AbortController();
|
||||
this.isLoading = true;
|
||||
const cosmetics = await getCachedCosmetics();
|
||||
if (!this.isConnected) return;
|
||||
this.cosmetics = cosmetics;
|
||||
this.updateFromSettings();
|
||||
if (!this.isConnected) return;
|
||||
this.isLoading = false;
|
||||
window.addEventListener("pattern-selected", this._onPatternSelected, {
|
||||
signal: this._abortController.signal,
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this._abortController) {
|
||||
this._abortController.abort();
|
||||
this._abortController = null;
|
||||
}
|
||||
}
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
const isDefault = this.pattern === null && this.selectedColor === null;
|
||||
const showSelect = this.showSelectLabel && isDefault;
|
||||
const buttonTitle = translateText("territory_patterns.title");
|
||||
|
||||
// Show loading state
|
||||
if (this.isLoading) {
|
||||
return html`
|
||||
<button
|
||||
id="pattern-input"
|
||||
class="pattern-btn m-0 border-0 !p-0 w-full h-full flex cursor-pointer justify-center items-center focus:outline-none focus:ring-0 bg-slate-900/80 rounded-lg overflow-hidden"
|
||||
disabled
|
||||
>
|
||||
<span
|
||||
class="w-6 h-6 border-4 border-blue-500/30 border-t-blue-500 rounded-full animate-spin"
|
||||
></span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
let previewContent;
|
||||
if (this.pattern) {
|
||||
previewContent = renderPatternPreview(this.pattern, 128, 128);
|
||||
} else {
|
||||
previewContent = renderPatternPreview(null, 128, 128);
|
||||
}
|
||||
|
||||
return html`
|
||||
<button
|
||||
id="pattern-input"
|
||||
class="pattern-btn m-0 border-0 !p-0 w-full h-full flex cursor-pointer justify-center items-center focus:outline-none focus:ring-0 transition-all duration-200 hover:scale-105 bg-slate-900/80 hover:bg-slate-800/80 active:bg-slate-800/90 rounded-lg overflow-hidden"
|
||||
title=${buttonTitle}
|
||||
@click=${this.onInputClick}
|
||||
>
|
||||
<span
|
||||
class=${showSelect
|
||||
? "hidden"
|
||||
: "w-full h-full overflow-hidden flex items-center justify-center [&>img]:object-cover [&>img]:w-full [&>img]:h-full"}
|
||||
>
|
||||
${!showSelect ? previewContent : null}
|
||||
</span>
|
||||
${showSelect
|
||||
? html`<span
|
||||
class="text-[10px] font-black text-white/40 uppercase leading-none break-words w-full text-center px-1"
|
||||
>
|
||||
${translateText("territory_patterns.select_skin")}
|
||||
</span>`
|
||||
: null}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
}
|
||||
+104
-91
@@ -26,7 +26,7 @@ export class PublicLobby extends LitElement {
|
||||
|
||||
private joiningInterval: number | null = null;
|
||||
private currLobby: GameInfo | null = null;
|
||||
private debounceDelay: number = 750;
|
||||
private debounceDelay: number = 150;
|
||||
private lobbyIDToStart = new Map<GameID, number>();
|
||||
private lobbySocket = new PublicLobbySocket((lobbies) =>
|
||||
this.handleLobbiesUpdate(lobbies),
|
||||
@@ -124,111 +124,124 @@ export class PublicLobby extends LitElement {
|
||||
<button
|
||||
@click=${() => this.lobbyClicked(lobby)}
|
||||
?disabled=${this.isButtonDebounced}
|
||||
class="group relative isolate flex flex-col w-full h-80 lg:h-96 overflow-hidden rounded-2xl transition-all duration-300 ${this
|
||||
class="group relative isolate flex flex-col w-full h-80 lg:h-96 overflow-hidden rounded-2xl transition-all duration-200 bg-[#3d7bab] ${this
|
||||
.isLobbyHighlighted
|
||||
? "ring-2 ring-blue-600 scale-[1.01] opacity-70"
|
||||
: "hover:scale-[1.01] hover:border-white/30"} ${this.isButtonDebounced
|
||||
? "opacity-70 cursor-not-allowed"
|
||||
: "hover:scale-[1.01]"} active:scale-[0.98] ${this.isButtonDebounced
|
||||
? "cursor-not-allowed"
|
||||
: ""}"
|
||||
>
|
||||
<!-- Map Image Area -->
|
||||
<div class="flex-1 w-full relative overflow-hidden bg-blue-500/85">
|
||||
${mapImageSrc
|
||||
? html`<img
|
||||
src="${mapImageSrc}"
|
||||
alt="${lobby.gameConfig.gameMap}"
|
||||
class="w-full h-full object-cover filter drop-shadow-2xl"
|
||||
/>`
|
||||
: html`<div class="w-full h-full bg-gray-800 rounded-lg"></div>`}
|
||||
</div>
|
||||
<div class="font-sans w-full h-full flex flex-col">
|
||||
<!-- Main card gradient - stops before text -->
|
||||
<div class="absolute inset-0 pointer-events-none z-10"></div>
|
||||
|
||||
<!-- Content Banner -->
|
||||
<div
|
||||
class="relative w-full p-5 flex flex-col gap-1 text-left z-10 bg-slate-900/95 backdrop-blur-xl border-t border-white/10"
|
||||
>
|
||||
<div class="flex justify-between items-end w-full">
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- Header: Status or Join -->
|
||||
<div
|
||||
class="text-sm font-bold uppercase tracking-widest text-blue-400 mb-1"
|
||||
<!-- Map Image Area with gradient overlay -->
|
||||
<div class="flex-1 w-full relative overflow-hidden">
|
||||
${mapImageSrc
|
||||
? html`<img
|
||||
src="${mapImageSrc}"
|
||||
alt="${lobby.gameConfig.gameMap}"
|
||||
class="absolute inset-0 w-full h-full object-cover object-center z-10"
|
||||
/>`
|
||||
: ""}
|
||||
<!-- Vignette overlay for dark edges -->
|
||||
<div class="pointer-events-none absolute inset-0 z-20"></div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Badge in top left -->
|
||||
${fullModeLabel
|
||||
? html`<span
|
||||
class="absolute top-4 left-4 px-4 py-1 rounded font-bold text-sm lg:text-base uppercase tracking-widest z-30 bg-slate-800 text-white ring-1 ring-white/10 shadow-sm"
|
||||
>
|
||||
${this.currLobby
|
||||
? isStarting
|
||||
? html`<span class="text-green-400 animate-pulse"
|
||||
>${translateText("public_lobby.starting_game")}</span
|
||||
>`
|
||||
: html`${translateText("public_lobby.waiting_for_players")}
|
||||
${[0, 1, 2]
|
||||
.map((i) => (i === this.joiningDotIndex ? "•" : "·"))
|
||||
.join("")}`
|
||||
: html`<span
|
||||
class="group-hover:text-blue-300 transition-colors"
|
||||
>${translateText("public_lobby.join")}</span
|
||||
>`}
|
||||
${fullModeLabel}
|
||||
</span>`
|
||||
: ""}
|
||||
|
||||
<!-- Timer in top right -->
|
||||
${timeRemaining > 0
|
||||
? html`
|
||||
<span
|
||||
class="absolute top-4 right-4 px-4 py-1 rounded font-bold text-sm lg:text-base tracking-widest z-30 bg-blue-600 text-white"
|
||||
>
|
||||
${timeDisplay}
|
||||
</span>
|
||||
`
|
||||
: html`<span
|
||||
class="absolute top-4 right-4 px-4 py-1 rounded font-bold text-sm lg:text-base uppercase tracking-widest z-30 bg-green-600 text-white"
|
||||
>
|
||||
${translateText("public_lobby.started")}
|
||||
</span>`}
|
||||
|
||||
<!-- Content Banner -->
|
||||
<div class="absolute bottom-0 left-0 right-0 z-20">
|
||||
<!-- Modifier badges placed just above the gradient overlay -->
|
||||
${modifierLabel.length > 0
|
||||
? html`<div
|
||||
class="absolute -top-8 left-4 z-30 flex gap-2 flex-wrap"
|
||||
>
|
||||
${modifierLabel.map(
|
||||
(label) => html`
|
||||
<span
|
||||
class="px-2 py-0.5 rounded text-xs font-medium uppercase tracking-wide bg-purple-600 text-white"
|
||||
>
|
||||
${label}
|
||||
</span>
|
||||
`,
|
||||
)}
|
||||
</div>`
|
||||
: html``}
|
||||
|
||||
<!-- Gradient overlay for text area - adds extra darkening -->
|
||||
<div
|
||||
class="absolute inset-0 bg-gradient-to-b from-black/60 to-black/90 pointer-events-none"
|
||||
></div>
|
||||
|
||||
<div class="relative p-6 flex flex-col gap-2 text-left">
|
||||
<!-- Header row: Status/Join on left, Player Count on right -->
|
||||
<div class="flex items-center justify-between w-full">
|
||||
<div class="text-base uppercase tracking-widest text-white">
|
||||
${this.currLobby
|
||||
? isStarting
|
||||
? html`<span class="text-green-400 animate-pulse"
|
||||
>${translateText("public_lobby.starting_game")}</span
|
||||
>`
|
||||
: html`<span class="text-orange-400"
|
||||
>${translateText("public_lobby.waiting_for_players")}
|
||||
${[0, 1, 2]
|
||||
.map((i) =>
|
||||
i === this.joiningDotIndex ? "•" : "·",
|
||||
)
|
||||
.join("")}</span
|
||||
>`
|
||||
: html`${translateText("public_lobby.join")}`}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2 text-white z-30">
|
||||
<span class="text-base font-bold uppercase tracking-widest"
|
||||
>${lobby.numClients}/${lobby.gameConfig.maxPlayers}</span
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5 text-white"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Map Name & Mode -->
|
||||
<!-- Map Name - Full Width -->
|
||||
<div
|
||||
class="text-3xl font-black text-white leading-none tracking-tight"
|
||||
class="text-2xl lg:text-3xl font-bold text-white leading-none uppercase tracking-widest w-full"
|
||||
>
|
||||
${translateText(
|
||||
`map.${lobby.gameConfig.gameMap.toLowerCase().replace(/[\s.]+/g, "")}`,
|
||||
)}
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2 mt-2">
|
||||
${fullModeLabel
|
||||
? html`<span
|
||||
class="px-2 py-1 rounded text-xs font-bold uppercase tracking-wider ${this
|
||||
.isLobbyHighlighted
|
||||
? "bg-green-500/20 text-green-300 border border-green-500/30"
|
||||
: "bg-white/10 text-white border border-white/10"} backdrop-blur-sm"
|
||||
>
|
||||
${fullModeLabel}
|
||||
</span>`
|
||||
: ""}
|
||||
${modifierLabel.map(
|
||||
(label) =>
|
||||
html`<span
|
||||
class="px-2 py-1 rounded text-xs font-bold uppercase tracking-wider ${this
|
||||
.isLobbyHighlighted
|
||||
? "bg-green-500/20 text-green-300 border border-green-500/30"
|
||||
: "bg-white/10 text-white border border-white/10"} backdrop-blur-sm"
|
||||
>
|
||||
${label}
|
||||
</span>`,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Player Count & Time -->
|
||||
<div class="flex flex-col items-end gap-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-2xl font-bold text-white"
|
||||
>${lobby.numClients}/${lobby.gameConfig.maxPlayers}</span
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5 text-white/50"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
${timeRemaining > 0
|
||||
? html`
|
||||
<div
|
||||
class="text-sm font-mono font-medium text-blue-200 bg-blue-500/20 px-2 py-0.5 rounded border border-blue-500/30"
|
||||
>
|
||||
${timeDisplay}
|
||||
</div>
|
||||
`
|
||||
: html`<div
|
||||
class="text-sm font-bold text-green-200 bg-green-500/20 border border-green-500/30 px-2 py-0.5 rounded uppercase tracking-wider"
|
||||
>
|
||||
${translateText("public_lobby.started")}
|
||||
</div>`}
|
||||
<!-- modifiers moved above gradient overlay -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -129,7 +129,7 @@ export class SinglePlayerModal extends BaseModal {
|
||||
render() {
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full flex flex-col bg-black/40 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden"
|
||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 overflow-hidden"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div
|
||||
@@ -599,7 +599,7 @@ export class SinglePlayerModal extends BaseModal {
|
||||
min="1"
|
||||
max="120"
|
||||
.value=${String(this.maxTimerValue ?? "")}
|
||||
class="w-full text-center rounded bg-black/40 text-white text-sm font-bold border border-white/20 focus:outline-none focus:border-blue-500 p-1 my-1"
|
||||
class="w-full text-center rounded bg-black/60 text-white text-sm font-bold border border-white/20 focus:outline-none focus:border-blue-500 p-1 my-1"
|
||||
aria-label=${translateText("single_modal.max_timer")}
|
||||
@input=${this.handleMaxTimerValueChanges}
|
||||
@keydown=${this.handleMaxTimerValueKeyDown}
|
||||
|
||||
@@ -372,7 +372,7 @@ export class StatsModal extends BaseModal {
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full flex flex-col ${this.inline
|
||||
? "bg-black/40 backdrop-blur-md rounded-2xl border border-white/10"
|
||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10"
|
||||
: ""}"
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, render } from "lit";
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import { ColorPalette, Cosmetics, Pattern } from "../core/CosmeticSchemas";
|
||||
@@ -9,7 +9,6 @@ import { hasLinkedAccount } from "./Api";
|
||||
import { BaseModal } from "./components/BaseModal";
|
||||
import "./components/Difficulties";
|
||||
import "./components/PatternButton";
|
||||
import { renderPatternPreview } from "./components/PatternButton";
|
||||
import {
|
||||
fetchCosmetics,
|
||||
handlePurchase,
|
||||
@@ -279,7 +278,7 @@ export class TerritoryPatternsModal extends BaseModal {
|
||||
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full flex flex-col bg-black/40 backdrop-blur-md rounded-2xl border border-white/10 p-6"
|
||||
class="h-full flex flex-col bg-black/60 backdrop-blur-md rounded-2xl border border-white/10 p-6"
|
||||
>
|
||||
${this.renderTabNavigation()}
|
||||
<div class="overflow-y-auto pr-2 custom-scrollbar mr-1">
|
||||
@@ -391,6 +390,7 @@ export class TerritoryPatternsModal extends BaseModal {
|
||||
this.selectedColor = hexCode;
|
||||
this.userSettings.setSelectedColor(hexCode);
|
||||
this.refresh();
|
||||
this.dispatchEvent(new CustomEvent("pattern-selected", { bubbles: true }));
|
||||
this.close();
|
||||
}
|
||||
|
||||
@@ -409,33 +409,5 @@ export class TerritoryPatternsModal extends BaseModal {
|
||||
|
||||
public async refresh() {
|
||||
this.requestUpdate();
|
||||
|
||||
const preview = this.selectedColor
|
||||
? this.renderColorPreview(this.selectedColor, 48, 48)
|
||||
: renderPatternPreview(this.selectedPattern ?? null, 48, 48);
|
||||
|
||||
if (
|
||||
this.previewButton === null ||
|
||||
!document.body.contains(this.previewButton)
|
||||
) {
|
||||
this.previewButton = document.getElementById(
|
||||
"territory-patterns-input-preview-button",
|
||||
);
|
||||
}
|
||||
|
||||
if (this.previewButton === null) return;
|
||||
|
||||
// Check if the element is still in the DOM to avoid lit-html errors
|
||||
if (!document.body.contains(this.previewButton)) {
|
||||
console.warn(
|
||||
"TerritoryPatternsModal: previewButton is disconnected from DOM, skipping render",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Clear and re-render using Lit
|
||||
render(preview, this.previewButton);
|
||||
this.previewButton.style.padding = "4px";
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { translateText } from "../client/Utils";
|
||||
import { formatKeyForDisplay, translateText } from "../client/Utils";
|
||||
import { UserSettings } from "../core/game/UserSettings";
|
||||
import "./components/baseComponents/setting/SettingKeybind";
|
||||
import { SettingKeybind } from "./components/baseComponents/setting/SettingKeybind";
|
||||
import "./components/baseComponents/setting/SettingNumber";
|
||||
import "./components/baseComponents/setting/SettingSlider";
|
||||
import "./components/baseComponents/setting/SettingToggle";
|
||||
@@ -13,18 +15,203 @@ interface FlagInputModalElement extends HTMLElement {
|
||||
returnTo?: string;
|
||||
}
|
||||
|
||||
const DefaultKeybinds: Record<string, string> = {
|
||||
toggleView: "Space",
|
||||
buildCity: "Digit1",
|
||||
buildFactory: "Digit2",
|
||||
buildPort: "Digit3",
|
||||
buildDefensePost: "Digit4",
|
||||
buildMissileSilo: "Digit5",
|
||||
buildSamLauncher: "Digit6",
|
||||
buildWarship: "Digit7",
|
||||
buildAtomBomb: "Digit8",
|
||||
buildHydrogenBomb: "Digit9",
|
||||
buildMIRV: "Digit0",
|
||||
attackRatioDown: "KeyT",
|
||||
attackRatioUp: "KeyY",
|
||||
boatAttack: "KeyB",
|
||||
groundAttack: "KeyG",
|
||||
zoomOut: "KeyQ",
|
||||
zoomIn: "KeyE",
|
||||
centerCamera: "KeyC",
|
||||
moveUp: "KeyW",
|
||||
moveLeft: "KeyA",
|
||||
moveDown: "KeyS",
|
||||
moveRight: "KeyD",
|
||||
};
|
||||
|
||||
@customElement("user-setting")
|
||||
export class UserSettingModal extends BaseModal {
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
@state() private activeTab: "basic" | "keybinds" = "basic";
|
||||
|
||||
@state() private keySequence: string[] = [];
|
||||
@state() private showEasterEggSettings = false;
|
||||
|
||||
@state() private keybinds: Record<
|
||||
string,
|
||||
{ value: string | string[]; key: string }
|
||||
> = {};
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.loadKeybindsFromStorage();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
window.removeEventListener("keydown", this.handleEasterEggKey);
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private loadKeybindsFromStorage() {
|
||||
const savedKeybinds = localStorage.getItem("settings.keybinds");
|
||||
if (!savedKeybinds) return;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(savedKeybinds);
|
||||
if (
|
||||
typeof parsed === "object" &&
|
||||
parsed !== null &&
|
||||
!Array.isArray(parsed)
|
||||
) {
|
||||
const isValid = Object.values(parsed).every((entry) => {
|
||||
if (
|
||||
typeof entry !== "object" ||
|
||||
entry === null ||
|
||||
Array.isArray(entry)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!("key" in entry) || typeof (entry as any).key !== "string") {
|
||||
return false;
|
||||
}
|
||||
if (!("value" in entry)) {
|
||||
return false;
|
||||
}
|
||||
const value = (entry as any).value;
|
||||
if (typeof value === "string") {
|
||||
return true;
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.every((v) => typeof v === "string");
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
this.keybinds = parsed;
|
||||
} else {
|
||||
console.warn(
|
||||
"Invalid keybinds structure: entries must be objects with 'key' (string) and 'value' (string or string[]) properties. Ignoring saved data.",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Invalid keybinds data: expected non-array object. Ignoring saved data.",
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Invalid keybinds JSON:", e);
|
||||
}
|
||||
}
|
||||
|
||||
private handleKeybindChange(
|
||||
e: CustomEvent<{
|
||||
action: string;
|
||||
value: string;
|
||||
key: string;
|
||||
prevValue?: string;
|
||||
}>,
|
||||
) {
|
||||
const { action, value, key, prevValue } = e.detail;
|
||||
|
||||
const activeKeybinds: Record<string, string> = { ...DefaultKeybinds };
|
||||
for (const [k, v] of Object.entries(this.keybinds)) {
|
||||
const normalizedValue = Array.isArray(v.value)
|
||||
? v.value[0] || ""
|
||||
: v.value;
|
||||
if (normalizedValue === "Null") {
|
||||
delete activeKeybinds[k];
|
||||
} else {
|
||||
activeKeybinds[k] = normalizedValue;
|
||||
}
|
||||
}
|
||||
|
||||
const values = Object.entries(activeKeybinds)
|
||||
.filter(([k]) => k !== action)
|
||||
.map(([, v]) => v);
|
||||
|
||||
if (values.includes(value) && value !== "Null") {
|
||||
const displayKey = formatKeyForDisplay(key || value);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show-message", {
|
||||
detail: {
|
||||
message: html`
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-6 w-6 text-red-500 inline-block align-middle mr-2"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
<span class="font-medium">
|
||||
${(() => {
|
||||
const message = translateText(
|
||||
"user_setting.keybind_conflict_error",
|
||||
{ key: displayKey },
|
||||
);
|
||||
const parts = message.split(displayKey);
|
||||
return html`${parts[0]}<span
|
||||
class="font-mono font-bold bg-white/10 px-1.5 py-0.5 rounded text-red-200 mx-1 border border-white/10"
|
||||
>${displayKey}</span
|
||||
>${parts[1] || ""}`;
|
||||
})()}
|
||||
</span>
|
||||
`,
|
||||
color: "red",
|
||||
duration: 3000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const element = this.renderRoot.querySelector(
|
||||
`setting-keybind[action="${action}"]`,
|
||||
) as SettingKeybind;
|
||||
if (element) {
|
||||
element.value = prevValue ?? DefaultKeybinds[action] ?? "";
|
||||
element.requestUpdate();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this.keybinds = { ...this.keybinds, [action]: { value: value, key: key } };
|
||||
localStorage.setItem("settings.keybinds", JSON.stringify(this.keybinds));
|
||||
}
|
||||
|
||||
private getKeyValue(action: string): string | undefined {
|
||||
const entry = this.keybinds[action];
|
||||
if (!entry) return undefined;
|
||||
const normalizedValue = Array.isArray(entry.value)
|
||||
? entry.value[0] || ""
|
||||
: entry.value;
|
||||
if (normalizedValue === "Null") return "";
|
||||
return normalizedValue || undefined;
|
||||
}
|
||||
|
||||
private getKeyChar(action: string): string {
|
||||
const entry = this.keybinds[action];
|
||||
if (!entry) return "";
|
||||
return entry.key || "";
|
||||
}
|
||||
|
||||
private handleEasterEggKey = (e: KeyboardEvent) => {
|
||||
if (!this.isModalOpen || this.showEasterEggSettings) return;
|
||||
|
||||
@@ -188,20 +375,25 @@ export class UserSettingModal extends BaseModal {
|
||||
document.querySelector<FlagInputModalElement>("#flag-input-modal");
|
||||
if (flagInputModal?.open) {
|
||||
this.close();
|
||||
flagInputModal.returnTo = "#" + (this.id || "page-options");
|
||||
flagInputModal.returnTo = "#" + (this.id || "page-settings");
|
||||
flagInputModal.open();
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const activeContent =
|
||||
this.activeTab === "basic"
|
||||
? this.renderBasicSettings()
|
||||
: this.renderKeybindSettings();
|
||||
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full flex flex-col ${this.inline
|
||||
? "bg-black/40 backdrop-blur-md rounded-2xl border border-white/10"
|
||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10"
|
||||
: ""}"
|
||||
>
|
||||
<div
|
||||
class="flex items-center mb-6 pb-2 border-b border-white/10 gap-2 shrink-0 p-6"
|
||||
class="relative flex flex-col mb-6 border-b border-white/10 pb-4 shrink-0 p-6"
|
||||
>
|
||||
<div class="flex items-center gap-4 flex-1 flex-wrap">
|
||||
<button
|
||||
@@ -230,12 +422,33 @@ export class UserSettingModal extends BaseModal {
|
||||
${translateText("user_setting.title")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="hidden md:flex items-center gap-2 justify-center mt-4">
|
||||
<button
|
||||
class="px-6 py-2 text-xs font-bold transition-all duration-200 rounded-lg uppercase tracking-widest ${this
|
||||
.activeTab === "basic"
|
||||
? "bg-blue-500/20 text-blue-400 border border-blue-500/30 shadow-[0_0_15px_rgba(59,130,246,0.2)]"
|
||||
: "text-white/40 hover:text-white hover:bg-white/5 border border-transparent"}"
|
||||
@click=${() => (this.activeTab = "basic")}
|
||||
>
|
||||
${translateText("user_setting.tab_basic")}
|
||||
</button>
|
||||
<button
|
||||
class="px-6 py-2 text-xs font-bold transition-all duration-200 rounded-lg uppercase tracking-widest ${this
|
||||
.activeTab === "keybinds"
|
||||
? "bg-blue-500/20 text-blue-400 border border-blue-500/30 shadow-[0_0_15px_rgba(59,130,246,0.2)]"
|
||||
: "text-white/40 hover:text-white hover:bg-white/5 border border-transparent"}"
|
||||
@click=${() => (this.activeTab = "keybinds")}
|
||||
>
|
||||
${translateText("user_setting.tab_keybinds")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent px-6 pb-6 mr-1"
|
||||
>
|
||||
<div class="flex flex-col gap-2">${this.renderBasicSettings()}</div>
|
||||
<div class="flex flex-col gap-2">${activeContent}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -260,6 +473,266 @@ export class UserSettingModal extends BaseModal {
|
||||
window.removeEventListener("keydown", this.handleEasterEggKey);
|
||||
}
|
||||
|
||||
private renderKeybindSettings() {
|
||||
return html`
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-4 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.view_options")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="toggleView"
|
||||
label=${translateText("user_setting.toggle_view")}
|
||||
description=${translateText("user_setting.toggle_view_desc")}
|
||||
defaultKey="Space"
|
||||
.value=${this.getKeyValue("toggleView")}
|
||||
.display=${this.getKeyChar("toggleView")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.build_controls")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="buildCity"
|
||||
label=${translateText("user_setting.build_city")}
|
||||
description=${translateText("user_setting.build_city_desc")}
|
||||
defaultKey="Digit1"
|
||||
.value=${this.getKeyValue("buildCity")}
|
||||
.display=${this.getKeyChar("buildCity")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildFactory"
|
||||
label=${translateText("user_setting.build_factory")}
|
||||
description=${translateText("user_setting.build_factory_desc")}
|
||||
defaultKey="Digit2"
|
||||
.value=${this.getKeyValue("buildFactory")}
|
||||
.display=${this.getKeyChar("buildFactory")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildPort"
|
||||
label=${translateText("user_setting.build_port")}
|
||||
description=${translateText("user_setting.build_port_desc")}
|
||||
defaultKey="Digit3"
|
||||
.value=${this.getKeyValue("buildPort")}
|
||||
.display=${this.getKeyChar("buildPort")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildDefensePost"
|
||||
label=${translateText("user_setting.build_defense_post")}
|
||||
description=${translateText("user_setting.build_defense_post_desc")}
|
||||
defaultKey="Digit4"
|
||||
.value=${this.getKeyValue("buildDefensePost")}
|
||||
.display=${this.getKeyChar("buildDefensePost")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildMissileSilo"
|
||||
label=${translateText("user_setting.build_missile_silo")}
|
||||
description=${translateText("user_setting.build_missile_silo_desc")}
|
||||
defaultKey="Digit5"
|
||||
.value=${this.getKeyValue("buildMissileSilo")}
|
||||
.display=${this.getKeyChar("buildMissileSilo")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildSamLauncher"
|
||||
label=${translateText("user_setting.build_sam_launcher")}
|
||||
description=${translateText("user_setting.build_sam_launcher_desc")}
|
||||
defaultKey="Digit6"
|
||||
.value=${this.getKeyValue("buildSamLauncher")}
|
||||
.display=${this.getKeyChar("buildSamLauncher")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildWarship"
|
||||
label=${translateText("user_setting.build_warship")}
|
||||
description=${translateText("user_setting.build_warship_desc")}
|
||||
defaultKey="Digit7"
|
||||
.value=${this.getKeyValue("buildWarship")}
|
||||
.display=${this.getKeyChar("buildWarship")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildAtomBomb"
|
||||
label=${translateText("user_setting.build_atom_bomb")}
|
||||
description=${translateText("user_setting.build_atom_bomb_desc")}
|
||||
defaultKey="Digit8"
|
||||
.value=${this.getKeyValue("buildAtomBomb")}
|
||||
.display=${this.getKeyChar("buildAtomBomb")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildHydrogenBomb"
|
||||
label=${translateText("user_setting.build_hydrogen_bomb")}
|
||||
description=${translateText("user_setting.build_hydrogen_bomb_desc")}
|
||||
defaultKey="Digit9"
|
||||
.value=${this.getKeyValue("buildHydrogenBomb")}
|
||||
.display=${this.getKeyChar("buildHydrogenBomb")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="buildMIRV"
|
||||
label=${translateText("user_setting.build_mirv")}
|
||||
description=${translateText("user_setting.build_mirv_desc")}
|
||||
defaultKey="Digit0"
|
||||
.value=${this.getKeyValue("buildMIRV")}
|
||||
.display=${this.getKeyChar("buildMIRV")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.attack_ratio_controls")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="attackRatioDown"
|
||||
label=${translateText("user_setting.attack_ratio_down")}
|
||||
description=${translateText("user_setting.attack_ratio_down_desc")}
|
||||
defaultKey="KeyT"
|
||||
.value=${this.getKeyValue("attackRatioDown")}
|
||||
.display=${this.getKeyChar("attackRatioDown")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="attackRatioUp"
|
||||
label=${translateText("user_setting.attack_ratio_up")}
|
||||
description=${translateText("user_setting.attack_ratio_up_desc")}
|
||||
defaultKey="KeyY"
|
||||
.value=${this.getKeyValue("attackRatioUp")}
|
||||
.display=${this.getKeyChar("attackRatioUp")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.attack_keybinds")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="boatAttack"
|
||||
label=${translateText("user_setting.boat_attack")}
|
||||
description=${translateText("user_setting.boat_attack_desc")}
|
||||
defaultKey="KeyB"
|
||||
.value=${this.getKeyValue("boatAttack")}
|
||||
.display=${this.getKeyChar("boatAttack")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="groundAttack"
|
||||
label=${translateText("user_setting.ground_attack")}
|
||||
description=${translateText("user_setting.ground_attack_desc")}
|
||||
defaultKey="KeyG"
|
||||
.value=${this.getKeyValue("groundAttack")}
|
||||
.display=${this.getKeyChar("groundAttack")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.zoom_controls")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="zoomOut"
|
||||
label=${translateText("user_setting.zoom_out")}
|
||||
description=${translateText("user_setting.zoom_out_desc")}
|
||||
defaultKey="KeyQ"
|
||||
.value=${this.getKeyValue("zoomOut")}
|
||||
.display=${this.getKeyChar("zoomOut")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="zoomIn"
|
||||
label=${translateText("user_setting.zoom_in")}
|
||||
description=${translateText("user_setting.zoom_in_desc")}
|
||||
defaultKey="KeyE"
|
||||
.value=${this.getKeyValue("zoomIn")}
|
||||
.display=${this.getKeyChar("zoomIn")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<h2
|
||||
class="text-blue-200 text-xl font-bold mt-8 mb-3 border-b border-white/10 pb-2"
|
||||
>
|
||||
${translateText("user_setting.camera_movement")}
|
||||
</h2>
|
||||
|
||||
<setting-keybind
|
||||
action="centerCamera"
|
||||
label=${translateText("user_setting.center_camera")}
|
||||
description=${translateText("user_setting.center_camera_desc")}
|
||||
defaultKey="KeyC"
|
||||
.value=${this.getKeyValue("centerCamera")}
|
||||
.display=${this.getKeyChar("centerCamera")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveUp"
|
||||
label=${translateText("user_setting.move_up")}
|
||||
description=${translateText("user_setting.move_up_desc")}
|
||||
defaultKey="KeyW"
|
||||
.value=${this.getKeyValue("moveUp")}
|
||||
.display=${this.getKeyChar("moveUp")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveLeft"
|
||||
label=${translateText("user_setting.move_left")}
|
||||
description=${translateText("user_setting.move_left_desc")}
|
||||
defaultKey="KeyA"
|
||||
.value=${this.getKeyValue("moveLeft")}
|
||||
.display=${this.getKeyChar("moveLeft")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveDown"
|
||||
label=${translateText("user_setting.move_down")}
|
||||
description=${translateText("user_setting.move_down_desc")}
|
||||
defaultKey="KeyS"
|
||||
.value=${this.getKeyValue("moveDown")}
|
||||
.display=${this.getKeyChar("moveDown")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
|
||||
<setting-keybind
|
||||
action="moveRight"
|
||||
label=${translateText("user_setting.move_right")}
|
||||
description=${translateText("user_setting.move_right_desc")}
|
||||
defaultKey="KeyD"
|
||||
.value=${this.getKeyValue("moveRight")}
|
||||
.display=${this.getKeyChar("moveRight")}
|
||||
@change=${this.handleKeybindChange}
|
||||
></setting-keybind>
|
||||
`;
|
||||
}
|
||||
|
||||
private renderBasicSettings() {
|
||||
return html`
|
||||
<!-- 🚩 Flag Selector -->
|
||||
@@ -450,6 +923,7 @@ export class UserSettingModal extends BaseModal {
|
||||
|
||||
protected onOpen(): void {
|
||||
window.addEventListener("keydown", this.handleEasterEggKey);
|
||||
this.loadKeybindsFromStorage();
|
||||
}
|
||||
|
||||
public open() {
|
||||
|
||||
@@ -63,7 +63,7 @@ export class UsernameInput extends LitElement {
|
||||
@input=${this.handleClanTagChange}
|
||||
placeholder="${translateText("username.tag")}"
|
||||
maxlength="5"
|
||||
class="w-20 bg-transparent border-b border-white/20 text-white placeholder-white/30 text-xl font-bold text-center focus:outline-none focus:border-white/50 transition-colors uppercase shrink-0"
|
||||
class="w-[6rem] bg-transparent border-b border-white/20 text-white placeholder-white/30 text-xl font-bold text-center focus:outline-none focus:border-white/50 transition-colors uppercase shrink-0"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
@@ -71,7 +71,7 @@ export class UsernameInput extends LitElement {
|
||||
@input=${this.handleUsernameChange}
|
||||
placeholder="${translateText("username.enter_username")}"
|
||||
maxlength="${MAX_USERNAME_LENGTH}"
|
||||
class="flex-1 min-w-0 bg-transparent border-0 text-white placeholder-white/30 text-2xl font-bold text-left focus:outline-none focus:ring-0 focus:bg-white/5 transition-colors overflow-x-auto whitespace-nowrap text-ellipsis pr-2"
|
||||
class="flex-1 min-w-0 bg-transparent border-0 text-white placeholder-white/30 text-2xl font-bold text-left focus:outline-none focus:ring-0 transition-colors overflow-x-auto whitespace-nowrap text-ellipsis pr-2"
|
||||
/>
|
||||
</div>
|
||||
${this.validationError
|
||||
|
||||
@@ -393,3 +393,29 @@ export async function getSvgAspectRatio(src: string): Promise<number | null> {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getDiscordAvatarUrl(user: {
|
||||
id: string;
|
||||
avatar: string | null;
|
||||
discriminator?: string;
|
||||
}): string | null {
|
||||
if (user.avatar) {
|
||||
// - id is a Discord numeric string
|
||||
// - avatar is a hash, optionally prefixed with "a_" for animated avatars
|
||||
const validId = /^\d+$/.test(user.id);
|
||||
const validAvatar =
|
||||
/^[a-f0-9]+$/.test(user.avatar) || /^a_[a-f0-9]+$/.test(user.avatar);
|
||||
|
||||
if (validId && validAvatar) {
|
||||
const extension = user.avatar.startsWith("a_") ? "gif" : "png";
|
||||
return `https://cdn.discordapp.com/avatars/${encodeURIComponent(user.id)}/${encodeURIComponent(user.avatar)}.${extension}?size=64`;
|
||||
}
|
||||
}
|
||||
|
||||
if (user.discriminator !== undefined) {
|
||||
const idx = Number(user.discriminator) % 5;
|
||||
return `https://cdn.discordapp.com/embed/avatars/${idx}.png`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("desktop-nav-bar")
|
||||
export class DesktopNavBar extends LitElement {
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("showPage", this._onShowPage);
|
||||
|
||||
const current = (window as any).currentPageId;
|
||||
if (current) {
|
||||
// Wait for render
|
||||
this.updateComplete.then(() => {
|
||||
this._updateActiveState(current);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("showPage", this._onShowPage);
|
||||
}
|
||||
|
||||
private _onShowPage = (e: Event) => {
|
||||
const pageId = (e as CustomEvent).detail;
|
||||
this._updateActiveState(pageId);
|
||||
};
|
||||
|
||||
private _updateActiveState(pageId: string) {
|
||||
this.querySelectorAll(".nav-menu-item").forEach((el) => {
|
||||
if ((el as HTMLElement).dataset.page === pageId) {
|
||||
el.classList.add("active");
|
||||
} else {
|
||||
el.classList.remove("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<nav
|
||||
class="hidden lg:flex w-full bg-slate-950/70 backdrop-blur-md border-b border-white/10 items-center justify-center gap-8 py-4 shrink-0 transition-opacity z-50 relative"
|
||||
>
|
||||
<div class="flex flex-col items-center justify-center">
|
||||
<div class="h-8 text-[#2563eb]">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1364 259"
|
||||
fill="currentColor"
|
||||
class="h-full w-auto drop-shadow-[0_0_10px_rgba(37,99,235,0.4)]"
|
||||
>
|
||||
<path
|
||||
d="M0,174V51h15.24v-17.14h16.81v-16.98h16.96V0h1266v17.23h17.13v16.81h16.98v16.96h14.88v123h-15.13v17.08h-17.08v17.08h-16.9v17.04H324.9v16.86h-16.9v16.95h-102v-17.12h-17.07v-17.05H48.73v-17.05h-16.89v-16.89H14.94v-16.89H0ZM1297.95,17.35H65.9v16.7h-17.08v17.08h-14.5v123.08h14.85v16.9h17.08v17.08h139.9v17.08h17.08v16.36h67.9v-16.72h17.08v-17.07h989.88v-17.07h17.08v-16.9h14.44V50.8h-14.75v-17.08h-16.9v-16.37Z"
|
||||
/>
|
||||
<path
|
||||
d="M189.1,154.78v17.07h-16.9v16.75h-51.07v-16.42h-16.9v-17.07h-16.97v-84.88h16.63v-17.07h16.9v-16.84h51.07v16.5h17.07v17.07h16.7v84.89h-16.54ZM137.87,53.1v17.15h-16.6v84.86h16.97v16.61h16.89v-16.97h16.6v-84.86h-16.97v-16.79h-16.89Z"
|
||||
/>
|
||||
<path
|
||||
d="M273.91,104.06v-16.73h50.92v16.45h16.85v68.05h-16.44v17.06h-50.96v16.88h16.4v16.96h-67.28v-16.61h16.33v-101.86h-16.38v-16.98h33.4v16.63c6.12,0,11.72,0,17.31,0,0,22.56,0,45.13,0,67.75h33.59v-67.61h-33.73Z"
|
||||
/>
|
||||
<path
|
||||
d="M631.12,188.64v-16.36h16.53V53.2h-16.25v-16.86h118.33v33.29h-16.65v-16.36h-50.72v50.44h33.36v-16.35h16.99v50.25h-16.6v-16.33h-33.73v50.65h16.37v16.72h-67.63Z"
|
||||
/>
|
||||
<path
|
||||
d="M596.78,103.8v84.94h-33.54v-84.39h-34.03v84.25h-33.85v-101.29h84.5v16.49h16.93Z"
|
||||
/>
|
||||
<path
|
||||
d="M1107.12,188.71v-84.34h-34.03v84.37h-33.7v-101.41h84.42v16.41h16.86v84.96h-33.54Z"
|
||||
/>
|
||||
<path
|
||||
d="M988.1,171.78v16.87h-67.88v-16.38h-16.87v-68.06h16.38v-16.87h68.06v16.38h16.87v68.06h-16.55ZM970.78,104.35h-33.39v67.38h33.39v-67.38Z"
|
||||
/>
|
||||
<path
|
||||
d="M460.77,155.38v16.49h-16.58v16.83h-68.05v-16.5h-16.83v-68.05h16.49v-16.83h68.05v16.49h16.83v34.06h-67.31v33.82h33.47v-16.31h33.92ZM393.39,104.18v16.56h33.3v-16.56h-33.3Z"
|
||||
/>
|
||||
<path
|
||||
d="M1209.13,172h-16.9v-67.9h-16.96v-16.9h16.68v-17.08h16.9v-16.82h16.9v33.58h50.98v16.91h-50.4v67.96h16.48v-16.43h50.95v16.54h-16.55v16.76h-68.08v-16.6Z"
|
||||
/>
|
||||
<path
|
||||
d="M834.91,120.94v16.96h-16.65v33.88h16.41v16.96h-67.29v-16.63h16.34v-67.87h-16.4v-16.97h50.42v33.81h17.3l-.14-.14Z"
|
||||
/>
|
||||
<path
|
||||
d="M835.05,121.08v-33.75h33.76v16.43h16.85v33.96h-33.43v-16.79c-6.13,0-11.73,0-17.32,0,0,0,.14.14.14.14Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div
|
||||
id="game-version"
|
||||
class="l-header__highlightText text-center"
|
||||
></div>
|
||||
</div>
|
||||
<!-- Desktop Navigation Menu Items -->
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-play"
|
||||
data-i18n="main.play"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-news"
|
||||
data-i18n="main.news"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500 relative"
|
||||
data-page="page-item-store"
|
||||
data-i18n="main.store"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-settings"
|
||||
data-i18n="main.settings"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-stats"
|
||||
data-i18n="main.stats"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item text-white/70 hover:text-blue-500 font-bold tracking-widest uppercase cursor-pointer transition-colors [&.active]:text-blue-500"
|
||||
data-page="page-help"
|
||||
data-i18n="main.help"
|
||||
></button>
|
||||
<lang-selector></lang-selector>
|
||||
<button
|
||||
id="nav-account-button"
|
||||
class="nav-menu-item relative h-10 rounded-full overflow-hidden flex items-center justify-center gap-2 px-3 bg-transparent border border-white/20 text-white/80 hover:text-white cursor-pointer transition-colors [&.active]:text-white"
|
||||
data-page="page-account"
|
||||
data-i18n-aria-label="main.account"
|
||||
data-i18n-title="main.account"
|
||||
>
|
||||
<img
|
||||
id="nav-account-avatar"
|
||||
class="hidden w-8 h-8 rounded-full object-cover"
|
||||
alt=""
|
||||
data-i18n-alt="main.discord_avatar_alt"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
<svg
|
||||
id="nav-account-person-icon"
|
||||
class="w-5 h-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M20 21a8 8 0 0 0-16 0" />
|
||||
<path d="M12 13a4 4 0 1 0-4-4 4 4 0 0 0 4 4Z" />
|
||||
</svg>
|
||||
<span
|
||||
id="nav-account-email-badge"
|
||||
class="hidden absolute bottom-1 right-1 w-4 h-4 rounded-full bg-slate-900/80 border border-white/20 flex items-center justify-center"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
class="w-2.5 h-2.5 text-white/80"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M4 4h16v16H4z" opacity="0" />
|
||||
<path d="M4 6h16v12H4z" />
|
||||
<path d="m4 7 8 6 8-6" />
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
id="nav-account-signin-text"
|
||||
class="text-xs font-bold tracking-widest"
|
||||
data-i18n="main.sign_in"
|
||||
>
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -106,7 +106,7 @@ export class FluentSlider extends LitElement {
|
||||
.min=${this.min}
|
||||
.max=${this.max}
|
||||
.valueAsNumber=${this.value}
|
||||
class="w-[60px] bg-black/40 text-white border border-white/20 text-center rounded text-sm p-1 leading-none font-bold font-inherit mt-1 focus:outline-none focus:border-blue-500"
|
||||
class="w-[60px] bg-black/60 text-white border border-white/20 text-center rounded text-sm p-1 leading-none font-bold font-inherit mt-1 focus:outline-none focus:border-blue-500"
|
||||
@input=${this.handleNumberInput}
|
||||
@blur=${() => {
|
||||
this.isEditing = false;
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("page-footer")
|
||||
export class Footer extends LitElement {
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<footer
|
||||
class="[.in-game_&]:hidden bg-black/60 backdrop-blur-md flex flex-col items-center justify-center gap-2 pt-[2px] pb-2 text-white/50 w-full border-t border-white/10 shrink-0 mt-auto"
|
||||
>
|
||||
<div class="flex items-center justify-center gap-6 pt-2">
|
||||
<a
|
||||
href="https://github.com/openfrontio/OpenFrontIO"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="opacity-60 hover:opacity-100 hover:scale-110 transition-all"
|
||||
>
|
||||
<img
|
||||
src="/icons/github-mark-white.svg"
|
||||
data-i18n-alt="news.github_link"
|
||||
class="h-7 w-7 object-contain"
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://www.reddit.com/r/OpenFrontIO/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="opacity-60 hover:opacity-100 hover:scale-110 transition-all"
|
||||
>
|
||||
<svg
|
||||
class="h-7 w-7 object-contain"
|
||||
viewBox="0 0 24 24"
|
||||
fill="white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.249-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://discord.gg/jRpxXvG42t"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="opacity-60 hover:opacity-100 hover:scale-110 transition-all"
|
||||
>
|
||||
<svg
|
||||
class="h-7 w-7 object-contain"
|
||||
viewBox="0 0 24 24"
|
||||
fill="white"
|
||||
>
|
||||
<path
|
||||
d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-14.36a.074.074 0 0 0-.032-.027zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.418 2.157-2.418 1.21 0 2.176 1.085 2.157 2.418 0 1.334-.956 2.419-2.157 2.419zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.418 2.157-2.418 1.21 0 2.176 1.085 2.157 2.418 0 1.334-.946 2.419-2.157 2.419z"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a
|
||||
href="https://openfront.wiki/Main_Page"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="opacity-60 hover:opacity-100 hover:scale-110 transition-all"
|
||||
>
|
||||
<img
|
||||
src="/icons/wiki-logo.svg"
|
||||
data-i18n-alt="main.wiki"
|
||||
class="h-7 w-7 object-contain"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-xs mt-2 flex items-center justify-center gap-4">
|
||||
<a
|
||||
href="/terms-of-service.html"
|
||||
data-i18n="main.terms_of_service"
|
||||
target="_blank"
|
||||
class="hover:text-white transition-colors"
|
||||
></a>
|
||||
<span data-i18n="main.copyright"></span>
|
||||
<a
|
||||
href="/privacy-policy.html"
|
||||
data-i18n="main.privacy_policy"
|
||||
target="_blank"
|
||||
class="hover:text-white transition-colors"
|
||||
></a>
|
||||
</div>
|
||||
</footer>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("main-layout")
|
||||
export class MainLayout extends LitElement {
|
||||
private _initialChildren: Node[] = [];
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
if (this._initialChildren.length === 0 && this.childNodes.length > 0) {
|
||||
this._initialChildren = Array.from(this.childNodes);
|
||||
}
|
||||
super.connectedCallback();
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<main
|
||||
class="relative [.in-game_&]:hidden flex flex-col flex-1 overflow-hidden w-full px-[clamp(1.5rem,3vw,3rem)] pt-[clamp(0.75rem,1.5vw,1.5rem)] pb-[clamp(0.75rem,1.5vw,1.5rem)]"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-[20cm] mx-auto flex flex-col flex-1 gap-[clamp(1.5rem,3vw,3rem)] overflow-y-auto overflow-x-hidden [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden lg:[scrollbar-width:auto] lg:[-ms-overflow-style:auto] lg:[&::-webkit-scrollbar]:block"
|
||||
>
|
||||
${this._initialChildren}
|
||||
</div>
|
||||
</main>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("mobile-nav-bar")
|
||||
export class MobileNavBar extends LitElement {
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("showPage", this._onShowPage);
|
||||
|
||||
const current = (window as any).currentPageId;
|
||||
if (current) {
|
||||
this.updateComplete.then(() => {
|
||||
this._updateActiveState(current);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("showPage", this._onShowPage);
|
||||
}
|
||||
|
||||
private _onShowPage = (e: Event) => {
|
||||
const pageId = (e as CustomEvent).detail;
|
||||
this._updateActiveState(pageId);
|
||||
};
|
||||
|
||||
private _updateActiveState(pageId: string) {
|
||||
this.querySelectorAll(".nav-menu-item").forEach((el) => {
|
||||
if ((el as HTMLElement).dataset.page === pageId) {
|
||||
el.classList.add("active");
|
||||
} else {
|
||||
el.classList.remove("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<!-- Border Segments (Custom right border with gap for button) -->
|
||||
<div
|
||||
class="absolute right-0 top-0 w-px bg-transparent"
|
||||
style="height: calc(50% - 64px)"
|
||||
></div>
|
||||
<div
|
||||
class="absolute right-0 bottom-0 w-px bg-transparent"
|
||||
style="height: calc(50% - 64px)"
|
||||
></div>
|
||||
|
||||
<div
|
||||
class="flex-1 w-full flex flex-col justify-start overflow-y-auto md:pt-[clamp(1rem,3vh,4rem)] md:pb-[clamp(0.5rem,2vh,2rem)] md:px-[clamp(1rem,1.5vw,2rem)] p-5 gap-[clamp(1rem,3vh,3rem)]"
|
||||
>
|
||||
<!-- Logo + Menu -->
|
||||
<div
|
||||
class="flex flex-col text-[#2563eb] mb-[clamp(1rem,2vh,2rem)] ml-[clamp(0.2rem,0.4vw,0.4vh)]"
|
||||
>
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1364 259"
|
||||
width="100%"
|
||||
height="100%"
|
||||
fill="currentColor"
|
||||
class="w-[clamp(120px,15vw,192px)] h-[clamp(40px,6vh,64px)] drop-shadow-[0_0_10px_rgba(37,99,235,0.3)]"
|
||||
>
|
||||
<!-- (Logo paths preserved) -->
|
||||
<path
|
||||
d="M0,174V51h15.24v-17.14h16.81v-16.98h16.96V0h1266v17.23h17.13v16.81h16.98v16.96h14.88v123h-15.13v17.08h-17.08v17.08h-16.9v17.04H324.9v16.86h-16.9v16.95h-102v-17.12h-17.07v-17.05H48.73v-17.05h-16.89v-16.89H14.94v-16.89H0ZM1297.95,17.35H65.9v16.7h-17.08v17.08h-14.5v123.08h14.85v16.9h17.08v17.08h139.9v17.08h17.08v16.36h67.9v-16.72h17.08v-17.07h989.88v-17.07h17.08v-16.9h14.44V50.8h-14.75v-17.08h-16.9v-16.37Z"
|
||||
/>
|
||||
<path
|
||||
d="M189.1,154.78v17.07h-16.9v16.75h-51.07v-16.42h-16.9v-17.07h-16.97v-84.88h16.63v-17.07h16.9v-16.84h51.07v16.5h17.07v17.07h16.7v84.89h-16.54ZM137.87,53.1v17.15h-16.6v84.86h16.97v16.61h16.89v-16.97h16.6v-84.86h-16.97v-16.79h-16.89Z"
|
||||
/>
|
||||
<path
|
||||
d="M273.91,104.06v-16.73h50.92v16.45h16.85v68.05h-16.44v17.06h-50.96v16.88h16.4v16.96h-67.28v-16.61h16.33v-101.86h-16.38v-16.98h33.4v16.63c6.12,0,11.72,0,17.31,0,0,22.56,0,45.13,0,67.75h33.59v-67.61h-33.73Z"
|
||||
/>
|
||||
<path
|
||||
d="M631.12,188.64v-16.36h16.53V53.2h-16.25v-16.86h118.33v33.29h-16.65v-16.36h-50.72v50.44h33.36v-16.35h16.99v50.25h-16.6v-16.33h-33.73v50.65h16.37v16.72h-67.63Z"
|
||||
/>
|
||||
<path
|
||||
d="M596.78,103.8v84.94h-33.54v-84.39h-34.03v84.25h-33.85v-101.29h84.5v16.49h16.93Z"
|
||||
/>
|
||||
<path
|
||||
d="M1107.12,188.71v-84.34h-34.03v84.37h-33.7v-101.41h84.42v16.41h16.86v84.96h-33.54Z"
|
||||
/>
|
||||
<path
|
||||
d="M988.1,171.78v16.87h-67.88v-16.38h-16.87v-68.06h16.38v-16.87h68.06v16.38h16.87v68.06h-16.55ZM970.78,104.35h-33.39v67.38h33.39v-67.38Z"
|
||||
/>
|
||||
<path
|
||||
d="M460.77,155.38v16.49h-16.58v16.83h-68.05v-16.5h-16.83v-68.05h16.49v-16.83h68.05v16.49h16.83v34.06h-67.31v33.82h33.47v-16.31h33.92ZM393.39,104.18v16.56h33.3v-16.56h-33.3Z"
|
||||
/>
|
||||
<path
|
||||
d="M1209.13,172h-16.9v-67.9h-16.96v-16.9h16.68v-17.08h16.9v-16.82h16.9v33.58h50.98v16.91h-50.4v67.96h16.48v-16.43h50.95v16.54h-16.55v16.76h-68.08v-16.6Z"
|
||||
/>
|
||||
<path
|
||||
d="M834.91,120.94v16.96h-16.65v33.88h16.41v16.96h-67.29v-16.63h16.34v-67.87h-16.4v-16.97h50.42v33.81h17.3l-.14-.14Z"
|
||||
/>
|
||||
<path
|
||||
d="M835.05,121.08v-33.75h33.76v16.43h16.85v33.96h-33.43v-16.79c-6.13,0-11.73,0-17.32,0,0,0,.14.14.14.14Z"
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
id="game-version"
|
||||
class="l-header__highlightText text-center"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile Navigation Menu Items -->
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-play"
|
||||
data-i18n="main.play"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-news"
|
||||
data-i18n="main.news"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-stats"
|
||||
data-i18n="main.stats"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-item-store"
|
||||
data-i18n="main.store"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-settings"
|
||||
data-i18n="main.settings"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-account"
|
||||
data-i18n="main.account"
|
||||
></button>
|
||||
<button
|
||||
class="nav-menu-item block w-full text-left font-bold uppercase tracking-[0.05em] text-white/70 transition-all duration-200 cursor-pointer hover:text-blue-600 hover:translate-x-2.5 hover:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] [&.active]:text-blue-600 [&.active]:translate-x-2.5 [&.active]:drop-shadow-[0_0_20px_rgba(37,99,235,0.5)] text-[clamp(18px,2.8vh,32px)] py-[clamp(0.2rem,0.8vh,0.75rem)]"
|
||||
data-page="page-help"
|
||||
data-i18n="main.help"
|
||||
></button>
|
||||
<div
|
||||
class="flex flex-col w-full mt-auto [.in-game_&]:hidden items-end justify-end pt-4 border-t border-white/10"
|
||||
>
|
||||
<lang-selector></lang-selector>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -188,7 +188,7 @@ function renderBlankPreview(width: number, height: number): TemplateResult {
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="hidden md:flex items-center justify-center h-full w-full bg-white/5 rounded overflow-hidden relative border border-white/10 box-border text-center p-1"
|
||||
class="hidden md:flex items-center justify-center h-full w-full rounded overflow-hidden relative text-center p-1"
|
||||
>
|
||||
<span
|
||||
class="text-[10px] font-black text-white/40 uppercase leading-none break-words w-full"
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
|
||||
@customElement("play-page")
|
||||
export class PlayPage extends LitElement {
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<div
|
||||
id="page-play"
|
||||
class="flex flex-col gap-2 w-full max-w-6xl mx-auto px-0 sm:px-4 transition-all duration-300 my-auto min-h-0"
|
||||
>
|
||||
<token-login class="w-full hidden"></token-login>
|
||||
|
||||
<!-- Header / Identity Section -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-2 lg:gap-6 w-full">
|
||||
<div
|
||||
class="lg:col-span-9 flex flex-row flex-nowrap gap-x-2 h-[60px] items-center bg-slate-900/80 backdrop-blur-md p-3 rounded-xl border border-blue-500/20 relative z-20 text-sm sm:text-base shrink-0"
|
||||
>
|
||||
<!-- Flag -->
|
||||
<div
|
||||
class="h-[40px] sm:h-[50px] shrink-0 aspect-[4/3] flex items-center justify-center lg:hidden"
|
||||
>
|
||||
<!-- Hamburger (Mobile) -->
|
||||
<button
|
||||
id="hamburger-btn"
|
||||
class="lg:hidden flex w-full h-full bg-slate-800/40 text-white/90 border border-blue-400/20 hover:bg-slate-700/40 p-0 rounded-md items-center justify-center cursor-pointer transition-all duration-200"
|
||||
data-i18n-aria-label="main.menu"
|
||||
aria-expanded="false"
|
||||
aria-controls="sidebar-menu"
|
||||
aria-haspopup="dialog"
|
||||
data-i18n-title="main.menu"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
class="w-8 h-8"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Username -->
|
||||
<div class="flex-1 min-w-0 h-[40px] sm:h-[50px] flex items-center">
|
||||
<username-input
|
||||
class="relative w-full h-full block text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
></username-input>
|
||||
</div>
|
||||
|
||||
<!-- Pattern button (Mobile - inside bar, Desktop - hidden here) -->
|
||||
<pattern-input
|
||||
id="pattern-input-mobile"
|
||||
show-select-label
|
||||
class="aspect-square h-[50px] sm:h-[50px] lg:hidden shrink-0"
|
||||
></pattern-input>
|
||||
</div>
|
||||
|
||||
<!-- Pattern & Flag buttons (Desktop only - separate column) -->
|
||||
<div class="hidden lg:flex lg:col-span-3">
|
||||
<div class="w-full h-[60px] flex gap-2">
|
||||
<pattern-input
|
||||
id="pattern-input-desktop"
|
||||
show-select-label
|
||||
class="flex-1 h-full"
|
||||
></pattern-input>
|
||||
<flag-input
|
||||
id="flag-input-desktop"
|
||||
show-select-label
|
||||
class="flex-1 h-full"
|
||||
></flag-input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Primary Game Actions Area -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6 w-full">
|
||||
<!-- Left Column: Featured Lobbies / Quick Play -->
|
||||
<div class="lg:col-span-9 flex flex-col gap-6 min-w-0">
|
||||
<!-- Public Lobby Card -->
|
||||
<public-lobby
|
||||
class="block w-full transition-all duration-[50ms]"
|
||||
></public-lobby>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Custom Games & Modes -->
|
||||
<div class="lg:col-span-3">
|
||||
<div
|
||||
class="group relative isolate flex flex-col w-full h-40 lg:h-96 overflow-hidden rounded-2xl transition-all duration-300"
|
||||
>
|
||||
<div
|
||||
class="h-full flex flex-col bg-slate-900/40 backdrop-blur-sm rounded-2xl border border-blue-400/10 overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="py-2 bg-blue-900/20 border-b border-blue-400/10 text-center text-sm font-bold text-gray-300 uppercase tracking-widest"
|
||||
data-i18n="host_modal.label"
|
||||
></div>
|
||||
<div class="flex-1 p-2 flex flex-row lg:flex-col gap-2">
|
||||
<o-button
|
||||
id="single-player"
|
||||
data-i18n-title="main.solo"
|
||||
translationKey="main.solo"
|
||||
fill
|
||||
class="flex-1 transition-transform"
|
||||
></o-button>
|
||||
|
||||
<o-button
|
||||
id="host-lobby-button"
|
||||
data-i18n-title="main.create"
|
||||
translationKey="main.create"
|
||||
fill
|
||||
secondary
|
||||
class="flex-1 opacity-90 hover:opacity-100"
|
||||
></o-button>
|
||||
|
||||
<o-button
|
||||
id="join-private-lobby-button"
|
||||
data-i18n-title="main.join"
|
||||
translationKey="main.join"
|
||||
fill
|
||||
secondary
|
||||
class="flex-1 opacity-90 hover:opacity-100"
|
||||
></o-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Matchmaking Buttons (Full Width across entire grid) -->
|
||||
<div class="lg:col-span-12 flex flex-col gap-6">
|
||||
<!-- Not Logged In Button -->
|
||||
<button
|
||||
id="matchmaking-button-logged-out"
|
||||
class="w-full h-20 bg-purple-600 hover:bg-purple-500 text-white font-black uppercase tracking-widest rounded-xl transition-all duration-200 flex flex-col items-center justify-center border border-purple-500/30 overflow-hidden relative cursor-pointer"
|
||||
>
|
||||
<span
|
||||
class="relative z-10 text-2xl"
|
||||
data-i18n="matchmaking_button.login_required"
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<!-- Logged In Button -->
|
||||
<button
|
||||
id="matchmaking-button"
|
||||
class="hidden w-full h-20 bg-purple-600 hover:bg-purple-500 text-white font-black uppercase tracking-widest rounded-xl transition-all duration-200 flex flex-col items-center justify-center border border-purple-500/30 group overflow-hidden relative"
|
||||
data-i18n-title="matchmaking_modal.title"
|
||||
>
|
||||
<span
|
||||
class="relative z-10 text-2xl"
|
||||
data-i18n="matchmaking_button.play_ranked"
|
||||
></span>
|
||||
<span
|
||||
class="relative z-10 text-xs font-medium text-purple-100 opacity-90 group-hover:opacity-100 transition-opacity"
|
||||
data-i18n="matchmaking_button.description"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -12,33 +12,36 @@ export class OButton extends LitElement {
|
||||
@property({ type: Boolean }) blockDesktop = false;
|
||||
@property({ type: Boolean }) disable = false;
|
||||
@property({ type: Boolean }) fill = false;
|
||||
private static readonly BASE_CLASS =
|
||||
"bg-blue-600 hover:bg-blue-700 text-white font-bold uppercase tracking-wider px-4 py-3 rounded-xl transition-all duration-300 transform hover:-translate-y-px outline-none border border-transparent text-center text-base lg:text-lg";
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
|
||||
private getButtonClasses(): Record<string, boolean> {
|
||||
return {
|
||||
[OButton.BASE_CLASS]: true,
|
||||
"w-full block": this.block,
|
||||
"h-full w-full flex items-center justify-center": this.fill,
|
||||
"lg:w-auto lg:inline-block":
|
||||
!this.block && !this.blockDesktop && !this.fill,
|
||||
"lg:w-1/2 lg:mx-auto lg:block": this.blockDesktop,
|
||||
"bg-gray-700 text-gray-100 hover:bg-gray-600": this.secondary,
|
||||
"disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none disabled:bg-gray-600":
|
||||
this.disable,
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<button
|
||||
class=${classMap({
|
||||
"bg-blue-600 hover:bg-blue-700 text-white font-bold uppercase tracking-wider px-4 py-3 rounded-xl transition-all duration-300 transform hover:-translate-y-px outline-none border border-transparent text-center text-base lg:text-lg":
|
||||
true,
|
||||
"dark:bg-blue-500 dark:hover:bg-blue-600": true,
|
||||
"w-full block": this.block,
|
||||
"h-full w-full flex items-center justify-center": this.fill,
|
||||
"lg:w-auto lg:inline-block":
|
||||
!this.block && !this.blockDesktop && !this.fill,
|
||||
"lg:w-1/2 lg:mx-auto lg:block": this.blockDesktop,
|
||||
"bg-blue-100 text-gray-900 hover:bg-blue-200 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600":
|
||||
this.secondary,
|
||||
"disabled:opacity-70 disabled:cursor-not-allowed disabled:transform-none disabled:bg-gray-600 dark:disabled:bg-gray-600":
|
||||
this.disable,
|
||||
})}
|
||||
class=${classMap(this.getButtonClasses())}
|
||||
?disabled=${this.disable}
|
||||
>
|
||||
${`${this.translationKey}` === ""
|
||||
? `${this.title}`
|
||||
: `${translateText(this.translationKey)}`}
|
||||
${this.translationKey === ""
|
||||
? this.title
|
||||
: translateText(this.translationKey)}
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export class SettingKeybind extends LitElement {
|
||||
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<div
|
||||
class="relative h-12 min-w-[80px] px-4 flex items-center justify-center bg-black/40 border border-white/20 rounded-lg text-xl font-bold font-mono shadow-inner hover:border-blue-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/50 transition-all cursor-pointer select-none text-white
|
||||
class="relative h-12 min-w-[80px] px-4 flex items-center justify-center bg-black/60 border border-white/20 rounded-lg text-xl font-bold font-mono shadow-inner hover:border-blue-500 focus:border-blue-500 focus:ring-2 focus:ring-blue-500/50 transition-all cursor-pointer select-none text-white
|
||||
${this.listening
|
||||
? "border-blue-500 text-blue-400 ring-2 ring-blue-500/50"
|
||||
: ""}"
|
||||
|
||||
@@ -50,7 +50,7 @@ export class SettingNumber extends LitElement {
|
||||
<input
|
||||
type="number"
|
||||
id="setting-number-input"
|
||||
class="shrink-0 w-[100px] py-2 px-3 border border-white/20 rounded-lg bg-black/40 text-white font-mono text-center focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all"
|
||||
class="shrink-0 w-[100px] py-2 px-3 border border-white/20 rounded-lg bg-black/60 text-white font-mono text-center focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 transition-all"
|
||||
.value=${String(this.value ?? 0)}
|
||||
min=${this.min}
|
||||
max=${this.max}
|
||||
|
||||
@@ -52,7 +52,7 @@ export class SettingToggle extends LitElement {
|
||||
@change=${this.handleChange}
|
||||
/>
|
||||
<span
|
||||
class="absolute inset-0 bg-black/40 border border-white/10 transition-all duration-300 rounded-full
|
||||
class="absolute inset-0 bg-black/60 border border-white/10 transition-all duration-300 rounded-full
|
||||
before:absolute before:content-[''] before:h-5 before:w-5 before:left-[3px] before:top-[3px]
|
||||
before:bg-white/40 before:transition-all before:duration-300 before:rounded-full before:shadow-sm hover:before:bg-white/60
|
||||
peer-checked:bg-blue-600 peer-checked:border-blue-500 peer-checked:before:translate-x-[24px] peer-checked:before:bg-white"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
import type { DiscordUser } from "../../../../core/ApiSchemas";
|
||||
import { translateText } from "../../../Utils";
|
||||
import { getDiscordAvatarUrl, translateText } from "../../../Utils";
|
||||
|
||||
@customElement("discord-user-header")
|
||||
export class DiscordUserHeader extends LitElement {
|
||||
@@ -23,15 +23,7 @@ export class DiscordUserHeader extends LitElement {
|
||||
private get avatarUrl(): string | null {
|
||||
const u = this._data;
|
||||
if (!u) return null;
|
||||
if (u.avatar) {
|
||||
const ext = u.avatar.startsWith("a_") ? "gif" : "png";
|
||||
return `https://cdn.discordapp.com/avatars/${u.id}/${u.avatar}.${ext}`;
|
||||
}
|
||||
if (u.discriminator !== undefined) {
|
||||
const idx = Number(u.discriminator) % 5;
|
||||
return `https://cdn.discordapp.com/embed/avatars/${idx}.png`;
|
||||
}
|
||||
return null;
|
||||
return getDiscordAvatarUrl(u);
|
||||
}
|
||||
|
||||
private get discordDisplayName(): string {
|
||||
|
||||
@@ -126,10 +126,13 @@ export class HeadsUpMessage extends LitElement implements Layer {
|
||||
${this.isVisible
|
||||
? html`
|
||||
<div
|
||||
class="flex items-center relative
|
||||
w-full justify-evenly h-8 lg:h-10 md:top-17.5 left-0 lg:left-4
|
||||
class="fixed top-[10%] left-1/2 -translate-x-1/2 z-[11000]
|
||||
inline-flex items-center justify-center h-8 lg:h-10
|
||||
w-fit max-w-[90vw]
|
||||
bg-gray-900/60 rounded-md lg:rounded-lg
|
||||
backdrop-blur-md text-white text-md lg:text-xl p-1 lg:p-2"
|
||||
backdrop-blur-md text-white text-md lg:text-xl px-3 lg:px-4
|
||||
text-center break-words"
|
||||
style="word-wrap: break-word; hyphens: auto;"
|
||||
@contextmenu=${(e: MouseEvent) => e.preventDefault()}
|
||||
>
|
||||
${this.getMessage()}
|
||||
|
||||
@@ -187,7 +187,7 @@ export class SettingsModal extends LitElement implements Layer {
|
||||
|
||||
return html`
|
||||
<div
|
||||
class="modal-overlay fixed inset-0 bg-black/50 backdrop-blur-xs z-2000 flex items-center justify-center p-4"
|
||||
class="modal-overlay fixed inset-0 bg-black/60 backdrop-blur-xs z-2000 flex items-center justify-center p-4"
|
||||
@contextmenu=${(e: Event) => e.preventDefault()}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/* Deprecated global modal styles.
|
||||
The component-scoped styles in src/client/components/baseComponents/Modal.ts
|
||||
are the single source of truth now. Removing global overrides so the
|
||||
component can control layout and internal scrolling behavior. */
|
||||
|
||||
/* Keep small helper rule for legacy button layout, remove global .c-modal rules */
|
||||
o-modal o-button {
|
||||
@media (min-width: 1024px) {
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user