Home UI experiment

This commit is contained in:
Arkadiusz Sygulski
2026-02-20 23:17:29 +01:00
parent b865e0af8f
commit 542c332002
3 changed files with 209 additions and 142 deletions
+4 -4
View File
@@ -120,12 +120,12 @@
</head>
<body
class="h-full select-none font-sans min-h-screen bg-cover bg-center bg-fixed transition-opacity duration-300 ease-in-out flex flex-row overflow-hidden"
class="h-full select-none font-sans min-h-screen bg-neutral-800 bg-cover bg-center bg-fixed transition-opacity duration-300 ease-in-out flex flex-row overflow-hidden"
>
<div id="hex-grid" class="fixed inset-0 -z-50 pointer-events-none">
<div
id="background-layer"
class="absolute inset-0 bg-cover bg-center opacity-50 [filter:brightness(1.0)] dark:[filter:sepia(0.2)_saturate(1.2)_hue-rotate(180deg)_brightness(0.9)]"
class="absolute inset-0 bg-cover bg-center opacity-30 [filter:brightness(1.0)] dark:[filter:sepia(0.2)_saturate(1.2)_hue-rotate(180deg)_brightness(0.9)]"
style="
background-image: url(&quot;/resources/images/background.webp&quot;);
"
@@ -134,14 +134,14 @@
class="absolute inset-0 bg-center bg-no-repeat bg-contain hidden lg:block"
style="
background-image: url(&quot;/resources/images/OpenFront.webp&quot;);
opacity: 0.25;
opacity: 0.5;
"
></div>
<div
class="absolute inset-0 bg-center bg-no-repeat bg-contain lg:hidden"
style="
background-image: url(&quot;/resources/images/OF.webp&quot;);
opacity: 0.25;
opacity: 0.5;
"
></div>
</div>
+193 -109
View File
@@ -18,7 +18,7 @@ import { SinglePlayerModal } from "./SinglePlayerModal";
import { terrainMapFileLoader } from "./TerrainMapFileLoader";
import { getMapName, renderDuration, translateText } from "./Utils";
const CARD_BG = "bg-[color-mix(in_oklab,var(--frenchBlue)_70%,black)]";
const CARD_BG = "bg-sky-950";
@customElement("game-mode-selector")
export class GameModeSelector extends LitElement {
@@ -79,62 +79,93 @@ export class GameModeSelector extends LitElement {
this.requestUpdate();
}
render() {
private getSortedLobbies(): PublicGameInfo[] {
const ffa = this.lobbies?.games?.["ffa"]?.[0];
const teams = this.lobbies?.games?.["team"]?.[0];
const special = this.lobbies?.games?.["special"]?.[0];
return [ffa, teams, special]
.filter((g): g is PublicGameInfo => !!g)
.sort((a, b) => a.startsAt - b.startsAt);
}
private getLobbyTitleContent(lobby: PublicGameInfo): string | TemplateResult {
if (lobby === this.lobbies?.games?.["special"]?.[0]) {
const subtitle = this.getLobbyTitle(lobby);
const mainTitle = translateText("mode_selector.special_title");
return subtitle
? html`
<span class="block">${mainTitle}</span>
<span class="block text-[10px] leading-tight text-white/70">
${subtitle}
</span>
`
: mainTitle;
}
return this.getLobbyTitle(lobby);
}
render() {
const sorted = this.getSortedLobbies();
const featured = sorted[0];
const upcoming = sorted.slice(1);
return html`
<div
class="grid grid-cols-1 lg:grid-cols-2 gap-4 w-[70%] lg:w-full mx-auto"
>
${ffa ? this.renderLobbyCard(ffa, this.getLobbyTitle(ffa)) : nothing}
${teams
? this.renderLobbyCard(teams, this.getLobbyTitle(teams))
<div class="flex flex-col w-[90%] lg:max-w-xl mx-auto">
<!-- Multiplayer Games -->
${featured
? html`<div
class="grid grid-cols-[minmax(0,2fr)_minmax(0,1fr)] gap-2"
>
${this.renderFeaturedLobbyCard(
featured,
this.getLobbyTitleContent(featured),
)}
<div class="flex flex-col gap-2">
${upcoming.map((lobby) =>
this.renderUpcomingLobbyCard(
lobby,
this.getLobbyTitleContent(lobby),
),
)}
</div>
</div>`
: nothing}
${special ? this.renderSpecialLobbyCard(special) : nothing}
${this.renderQuickActionsSection()}
<!-- Solo - Primary CTA -->
<div class="mt-4">${this.renderSingleplayerButton()}</div>
<!-- Advanced Options -->
<div class="mt-2">${this.renderSecondaryActions()}</div>
</div>
`;
}
private renderSpecialLobbyCard(lobby: PublicGameInfo) {
const subtitle = this.getLobbyTitle(lobby);
const mainTitle = translateText("mode_selector.special_title");
const titleContent = subtitle
? html`
<span class="block">${mainTitle}</span>
<span class="block text-[10px] leading-tight text-white/70">
${subtitle}
</span>
`
: mainTitle;
return this.renderLobbyCard(lobby, titleContent);
private renderSingleplayerButton() {
return html`
<button
@click=${this.openSinglePlayerModal}
class="flex items-center justify-center w-full h-14 lg:h-16 rounded-lg bg-sky-600 hover:bg-sky-500 active:bg-sky-700 transition-colors text-lg lg:text-xl font-bold text-white uppercase tracking-widest"
>
${translateText("main.solo")}
</button>
`;
}
private renderQuickActionsSection() {
private renderSecondaryActions() {
return html`
<div class="contents lg:flex lg:flex-col lg:gap-2 lg:h-56">
<div class="max-lg:order-first grid grid-cols-2 gap-2 h-20 lg:flex-1">
${this.renderSmallActionCard(
translateText("main.solo"),
this.openSinglePlayerModal,
)}
${this.renderSmallActionCard(
translateText("mode_selector.ranked_title"),
this.openRankedMenu,
)}
</div>
<div class="grid grid-cols-2 gap-2 h-20 lg:flex-1">
${this.renderSmallActionCard(
translateText("main.create"),
this.openHostLobby,
)}
${this.renderSmallActionCard(
translateText("main.join"),
this.openJoinLobby,
)}
</div>
<div class="grid grid-cols-3 gap-2 h-10 lg:h-12">
${this.renderSmallActionCard(
translateText("mode_selector.ranked_title"),
this.openRankedMenu,
)}
${this.renderSmallActionCard(
translateText("main.create"),
this.openHostLobby,
)}
${this.renderSmallActionCard(
translateText("main.join"),
this.openJoinLobby,
)}
</div>
`;
}
@@ -161,18 +192,92 @@ export class GameModeSelector extends LitElement {
(document.querySelector("join-lobby-modal") as JoinLobbyModal)?.open();
};
private renderSmallActionCard(title: string, onClick: () => void) {
private renderFeaturedLobbyCard(
lobby: PublicGameInfo,
titleContent: string | TemplateResult,
) {
const mapType = lobby.gameConfig!.gameMap as GameMapType;
const mapImageSrc = terrainMapFileLoader.getMapData(mapType).webpPath;
const timeRemaining = Math.max(
0,
Math.floor((lobby.startsAt - this.serverTimeOffset - Date.now()) / 1000),
);
const timeDisplay = renderDuration(timeRemaining);
const mapName = getMapName(lobby.gameConfig?.gameMap);
const modifierLabels = this.getModifierLabels(
lobby.gameConfig?.publicGameModifiers,
);
if (modifierLabels.length > 1) {
modifierLabels.sort((a, b) => a.length - b.length);
}
return html`
<button
@click=${onClick}
class="flex items-center justify-center w-full h-full rounded-xl ${CARD_BG} border-0 transition-transform hover:scale-[1.02] active:scale-[0.98] text-sm lg:text-base font-bold text-white uppercase tracking-wider text-center"
@click=${() => this.validateAndJoin(lobby)}
class="group relative w-full aspect-square text-white uppercase rounded-2xl overflow-hidden transition-transform duration-200 hover:scale-[1.01] active:scale-[0.99] ${CARD_BG}"
>
${title}
${mapImageSrc
? html`<img
src="${mapImageSrc}"
alt="${mapName ?? lobby.gameConfig?.gameMap ?? "map"}"
draggable="false"
class="absolute inset-0 w-full h-full object-contain object-center scale-[1.05] pointer-events-none"
/>`
: null}
<div
class="absolute inset-x-2 top-2 flex items-start justify-between gap-2"
>
${modifierLabels.length > 0
? html`<div class="flex flex-col items-start gap-1">
${modifierLabels.map(
(label) =>
html`<span
class="px-2.5 py-1 rounded text-xs font-bold uppercase tracking-widest bg-teal-600 text-white shadow-md"
>${label}</span
>`,
)}
</div>`
: html`<div></div>`}
<div class="shrink-0">
${timeRemaining > 0
? html`<span
class="text-xs font-bold uppercase tracking-widest bg-blue-600 px-2.5 py-1 rounded shadow-md"
>${timeDisplay}</span
>`
: html`<span
class="text-xs font-bold uppercase tracking-widest bg-green-600 px-2.5 py-1 rounded shadow-md"
>${translateText("public_lobby.starting_game")}</span
>`}
</div>
</div>
<div
class="absolute inset-x-0 bottom-0 flex items-center justify-between px-4 py-4 bg-black/60 backdrop-blur-sm"
>
<div class="flex flex-col gap-1 min-w-0">
<h3
class="text-lg lg:text-2xl font-extrabold uppercase tracking-wider text-left leading-tight"
>
${titleContent}
</h3>
${mapName
? html`<p
class="text-sm text-white/90 uppercase tracking-wider text-left font-medium"
>
${mapName}
</p>`
: ""}
</div>
<span
class="text-sm font-bold uppercase tracking-widest shrink-0 ml-2"
>
${lobby.numClients}/${lobby.gameConfig?.maxPlayers}
</span>
</div>
</button>
`;
}
private renderLobbyCard(
private renderUpcomingLobbyCard(
lobby: PublicGameInfo,
titleContent: string | TemplateResult,
) {
@@ -185,72 +290,40 @@ export class GameModeSelector extends LitElement {
const timeDisplay = renderDuration(timeRemaining);
const mapName = getMapName(lobby.gameConfig?.gameMap);
const modifierLabels = this.getModifierLabels(
lobby.gameConfig?.publicGameModifiers,
);
// Sort by length for visual consistency (shorter labels first)
if (modifierLabels.length > 1) {
modifierLabels.sort((a, b) => a.length - b.length);
}
return html`
<button
@click=${() => this.validateAndJoin(lobby)}
class="group flex flex-col w-full h-40 lg:h-56 text-white uppercase rounded-2xl overflow-hidden transition-transform duration-200 hover:scale-[1.02] active:scale-[0.98] ${CARD_BG}"
class="group relative w-full flex-1 text-white uppercase rounded-xl overflow-hidden transition-transform duration-200 hover:scale-[1.01] active:scale-[0.99] ${CARD_BG}"
>
<div class="relative flex-1 overflow-hidden ${CARD_BG}">
${mapImageSrc
? html`<img
src="${mapImageSrc}"
alt="${mapName ?? lobby.gameConfig?.gameMap ?? "map"}"
draggable="false"
class="absolute inset-0 w-full h-full object-contain object-center scale-[1.05] pointer-events-none"
/>`
: null}
<div
class="absolute inset-x-2 bottom-2 flex items-end justify-between gap-2"
>
${modifierLabels.length > 0
? html`<div class="flex flex-col items-start gap-1">
${modifierLabels.map(
(label) =>
html`<span
class="px-2 py-0.5 rounded text-[10px] font-medium uppercase tracking-wide bg-teal-600 text-white shadow-[0_0_6px_rgba(13,148,136,0.35)]"
>${label}</span
>`,
)}
</div>`
: html`<div></div>`}
<div class="shrink-0">
${timeRemaining > 0
? html`<span
class="text-[10px] font-bold uppercase tracking-widest bg-blue-600 px-2 py-0.5 rounded"
>${timeDisplay}</span
>`
: html`<span
class="text-[10px] font-bold uppercase tracking-widest bg-green-600 px-2 py-0.5 rounded"
>${translateText("public_lobby.starting_game")}</span
>`}
</div>
</div>
${mapImageSrc
? html`<img
src="${mapImageSrc}"
alt="${mapName ?? lobby.gameConfig?.gameMap ?? "map"}"
draggable="false"
class="absolute inset-0 w-full h-full object-contain object-center scale-[1.05] pointer-events-none"
/>`
: null}
<div class="absolute top-1 right-1">
${timeRemaining > 0
? html`<span
class="text-xs font-bold uppercase tracking-widest bg-blue-600 px-2 py-0.5 rounded shadow-md"
>${timeDisplay}</span
>`
: html`<span
class="text-xs font-bold uppercase tracking-widest bg-green-600 px-2 py-0.5 rounded shadow-md"
>${translateText("public_lobby.starting_game")}</span
>`}
</div>
<div class="flex items-center justify-between px-3 py-2">
<div class="flex flex-col gap-0.5 min-w-0">
<h3
class="text-sm lg:text-base font-bold uppercase tracking-wider text-left leading-tight"
>
${titleContent}
</h3>
${mapName
? html`<p
class="text-[10px] text-white/70 uppercase tracking-wider text-left"
>
${mapName}
</p>`
: ""}
</div>
<div
class="absolute inset-x-0 bottom-0 flex items-center justify-between px-2.5 py-2.5 bg-black/60 backdrop-blur-sm"
>
<h3
class="text-sm font-bold uppercase tracking-wider text-left leading-tight truncate"
>
${titleContent}
</h3>
<span
class="text-xs font-bold uppercase tracking-widest shrink-0 ml-2"
class="text-xs font-bold uppercase tracking-widest shrink-0 ml-1"
>
${lobby.numClients}/${lobby.gameConfig?.maxPlayers}
</span>
@@ -259,6 +332,17 @@ export class GameModeSelector extends LitElement {
`;
}
private renderSmallActionCard(title: string, onClick: () => void) {
return html`
<button
@click=${onClick}
class="flex items-center justify-center w-full h-full rounded-lg bg-slate-700 hover:bg-slate-600 active:bg-slate-800 transition-colors text-sm lg:text-base font-medium text-white uppercase tracking-wider text-center"
>
${title}
</button>
`;
}
private validateAndJoin(lobby: PublicGameInfo) {
if (!this.validateUsername()) return;
+12 -29
View File
@@ -17,7 +17,7 @@ export class PlayPage extends LitElement {
<!-- Mobile: Fixed top bar -->
<div
class="lg:hidden fixed left-0 right-0 top-0 z-40 pt-[env(safe-area-inset-top)] bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)] border-b border-white/10"
class="lg:hidden fixed left-0 right-0 top-0 z-40 pt-[env(safe-area-inset-top)] bg-[color-mix(in_oklab,var(--frenchBlue)_50%,black)] border-b border-white/10"
>
<div
class="grid grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center h-14 px-2 gap-2"
@@ -99,39 +99,22 @@ export class PlayPage extends LitElement {
</div>
</div>
<!-- Mobile: spacer for fixed top bar -->
<div class="lg:hidden h-[calc(env(safe-area-inset-top)+56px)]"></div>
<!-- Section 1: User Customization -->
<div
class="w-full pb-4 lg:pb-0 flex flex-col gap-0 lg:grid lg:grid-cols-12 lg:gap-2"
class="w-[90%] lg:max-w-xl mx-auto px-3 py-2.5 rounded-xl bg-[color-mix(in_oklab,var(--frenchBlue)_50%,black)] border border-white/10 overflow-visible relative z-20"
>
<!-- Mobile: spacer for fixed top bar -->
<div class="lg:hidden h-[calc(env(safe-area-inset-top)+56px)]"></div>
<div
class="px-2 py-2 bg-[color-mix(in_oklab,var(--frenchBlue)_75%,black)] border-y border-white/10 overflow-visible lg:col-span-9 lg:flex lg:items-center lg:gap-x-2 lg:h-[60px] lg:p-3 lg:relative lg:z-20 lg:border-y-0 lg:rounded-xl"
>
<div class="flex items-center gap-2 min-w-0 w-full">
<username-input
class="flex-1 min-w-0 h-10 lg:h-[50px]"
></username-input>
<pattern-input
id="pattern-input-mobile"
show-select-label
adaptive-size
class="shrink-0 lg:hidden"
></pattern-input>
</div>
</div>
<div class="hidden lg:flex lg:col-span-3 h-[60px] gap-2">
<div class="flex items-center gap-2 min-w-0 w-full">
<username-input class="flex-1 min-w-0 h-10"></username-input>
<pattern-input
id="pattern-input-desktop"
id="pattern-input"
show-select-label
class="flex-1 h-full"
adaptive-size
class="shrink-0"
></pattern-input>
<flag-input
id="flag-input-desktop"
show-select-label
class="flex-1 h-full"
></flag-input>
<flag-input id="flag-input" class="shrink-0 w-10 h-10"></flag-input>
</div>
</div>