mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 17:36:44 +00:00
eebe3a7dbc
## Description: While loading the main page we also load a lot of map manifests and thumbnails. On prod its especially extreme, because we don't have "featured maps" there (186 json requests!). With this PR we only load the files when the map display is in the viewport. On main.openfront.dev (main page load): <img width="425" height="539" alt="image" src="https://github.com/user-attachments/assets/156338d2-7a3f-4518-a726-cb3dec3df908" /> ## 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: FloPinguin
162 lines
5.7 KiB
TypeScript
162 lines
5.7 KiB
TypeScript
import { LitElement, html } from "lit";
|
|
import { customElement, property, state } from "lit/decorators.js";
|
|
import { Difficulty, GameMapType } from "../../../core/game/Game";
|
|
import { terrainMapFileLoader } from "../../TerrainMapFileLoader";
|
|
import { translateText } from "../../Utils";
|
|
|
|
@customElement("map-display")
|
|
export class MapDisplay extends LitElement {
|
|
@property({ type: String }) mapKey = "";
|
|
@property({ type: Boolean }) selected = false;
|
|
@property({ type: String }) translation: string = "";
|
|
@property({ type: Boolean }) showMedals = false;
|
|
@property({ attribute: false }) wins: Set<Difficulty> = new Set();
|
|
@state() private mapWebpPath: string | null = null;
|
|
@state() private mapName: string | null = null;
|
|
@state() private isLoading = true;
|
|
@state() private hasNations = true;
|
|
private observer: IntersectionObserver | null = null;
|
|
private dataLoaded = false;
|
|
|
|
createRenderRoot() {
|
|
return this;
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this.observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries.some((e) => e.isIntersecting) && !this.dataLoaded) {
|
|
this.dataLoaded = true;
|
|
this.loadMapData();
|
|
this.observer?.disconnect();
|
|
}
|
|
},
|
|
{ rootMargin: "200px" },
|
|
);
|
|
this.observer.observe(this);
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
this.observer?.disconnect();
|
|
this.observer = null;
|
|
super.disconnectedCallback();
|
|
}
|
|
|
|
private async loadMapData() {
|
|
if (!this.mapKey) return;
|
|
|
|
try {
|
|
this.isLoading = true;
|
|
const mapValue = GameMapType[this.mapKey as keyof typeof GameMapType];
|
|
const data = terrainMapFileLoader.getMapData(mapValue);
|
|
this.mapWebpPath = await data.webpPath();
|
|
const manifest = await data.manifest();
|
|
this.mapName = manifest.name;
|
|
this.hasNations =
|
|
Array.isArray(manifest.nations) && manifest.nations.length > 0;
|
|
} catch (error) {
|
|
console.error("Failed to load map data:", error);
|
|
} finally {
|
|
this.isLoading = false;
|
|
}
|
|
}
|
|
|
|
private handleKeydown(event: KeyboardEvent) {
|
|
// Trigger the same activation logic as click when Enter or Space is pressed
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
event.preventDefault();
|
|
// Dispatch a click event to maintain compatibility with parent click handlers
|
|
(event.target as HTMLElement).click();
|
|
}
|
|
}
|
|
|
|
private preventImageDrag(event: DragEvent) {
|
|
event.preventDefault();
|
|
}
|
|
|
|
render() {
|
|
return html`
|
|
<div
|
|
role="button"
|
|
tabindex="0"
|
|
aria-selected="${this.selected}"
|
|
aria-label="${this.translation ?? this.mapName ?? this.mapKey}"
|
|
@keydown="${this.handleKeydown}"
|
|
class="w-full h-full p-3 flex flex-col items-center justify-between rounded-xl border cursor-pointer transition-all duration-200 active:scale-95 gap-3 group ${this
|
|
.selected
|
|
? "bg-blue-500/20 border-blue-500/50 shadow-[0_0_15px_rgba(59,130,246,0.3)]"
|
|
: "bg-white/5 border-white/10 hover:bg-white/10 hover:border-white/20 hover:-translate-y-1"}"
|
|
>
|
|
${this.isLoading
|
|
? html`<div
|
|
class="w-full aspect-[2/1] text-white/40 transition-transform duration-200 rounded-lg bg-black/20 text-xs font-bold uppercase tracking-wider flex items-center justify-center animate-pulse"
|
|
>
|
|
${translateText("map_component.loading")}
|
|
</div>`
|
|
: this.mapWebpPath
|
|
? html`<div
|
|
class="w-full aspect-[2/1] relative overflow-hidden rounded-lg bg-black/20"
|
|
>
|
|
<img
|
|
src="${this.mapWebpPath}"
|
|
alt="${this.translation || this.mapName}"
|
|
draggable="false"
|
|
@dragstart=${this.preventImageDrag}
|
|
class="w-full h-full object-cover ${this.selected
|
|
? "opacity-100"
|
|
: "opacity-80"} group-hover:opacity-100 transition-opacity duration-200"
|
|
/>
|
|
</div>`
|
|
: html`<div
|
|
class="w-full aspect-[2/1] text-red-400 transition-transform duration-200 rounded-lg bg-red-500/10 text-xs font-bold uppercase tracking-wider flex items-center justify-center"
|
|
>
|
|
${translateText("map_component.error")}
|
|
</div>`}
|
|
${this.showMedals && this.hasNations
|
|
? html`<div class="flex gap-1 justify-center w-full">
|
|
${this.renderMedals()}
|
|
</div>`
|
|
: null}
|
|
<div
|
|
class="text-xs font-bold text-white uppercase tracking-wider text-center leading-tight break-words hyphens-auto"
|
|
>
|
|
${this.translation || this.mapName}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderMedals() {
|
|
const medalOrder: Difficulty[] = [
|
|
Difficulty.Easy,
|
|
Difficulty.Medium,
|
|
Difficulty.Hard,
|
|
Difficulty.Impossible,
|
|
];
|
|
const colors: Record<Difficulty, string> = {
|
|
[Difficulty.Easy]: "var(--medal-easy)",
|
|
[Difficulty.Medium]: "var(--medal-medium)",
|
|
[Difficulty.Hard]: "var(--medal-hard)",
|
|
[Difficulty.Impossible]: "var(--medal-impossible)",
|
|
};
|
|
const wins = this.readWins();
|
|
return medalOrder.map((medal) => {
|
|
const earned = wins.has(medal);
|
|
const mask =
|
|
"url('/images/MedalIconWhite.svg') no-repeat center / contain";
|
|
return html`<div
|
|
class="w-5 h-5 ${earned ? "opacity-100" : "opacity-25"}"
|
|
style="background-color:${colors[
|
|
medal
|
|
]}; mask: ${mask}; -webkit-mask: ${mask};"
|
|
title=${translateText(`difficulty.${medal.toLowerCase()}`)}
|
|
></div>`;
|
|
});
|
|
}
|
|
|
|
private readWins(): Set<Difficulty> {
|
|
return this.wins ?? new Set();
|
|
}
|
|
}
|