diff --git a/resources/lang/en.json b/resources/lang/en.json
index 00f699951..5dfce1e70 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -944,6 +944,9 @@
"favorite": "Add to favourites",
"favorites_empty": "Click the star on any map to favourite it",
"loading": "Loading...",
+ "no_results": "No maps found",
+ "search_maps": "Search maps...",
+ "search_results": "Search Results",
"unfavorite": "Remove from favourites"
},
"matchmaking_button": {
diff --git a/src/client/components/GameConfigSettings.ts b/src/client/components/GameConfigSettings.ts
index 39184d267..414807297 100644
--- a/src/client/components/GameConfigSettings.ts
+++ b/src/client/components/GameConfigSettings.ts
@@ -6,7 +6,7 @@ import {
nothing,
svg,
} from "lit";
-import { customElement, property } from "lit/decorators.js";
+import { customElement, property, state } from "lit/decorators.js";
import {
Difficulty,
Duos,
@@ -80,10 +80,18 @@ function renderSection(
titleKey: string,
content: TemplateResult | TemplateResult[],
sectionClass = "space-y-6",
+ headerAction?: TemplateResult,
): TemplateResult {
return html`
- ${renderSectionHeader(iconSvg, colorClass, bgClass, titleKey)} ${content}
+ ${renderSectionHeader(
+ iconSvg,
+ colorClass,
+ bgClass,
+ titleKey,
+ headerAction,
+ )}
+ ${content}
`;
}
@@ -139,6 +147,7 @@ function renderSectionHeader(
colorClass: string,
bgClass: string,
titleKey: string,
+ headerAction?: TemplateResult,
): TemplateResult {
return html`
@@ -157,6 +166,7 @@ function renderSectionHeader(
${translateText(titleKey)}
+ ${headerAction ? html`
${headerAction}
` : null}
`;
}
@@ -218,6 +228,7 @@ export interface GameConfigSettingsData {
export class GameConfigSettings extends LitElement {
@property({ attribute: false }) settings?: GameConfigSettingsData;
@property({ attribute: false }) sectionGapClass = "space-y-6";
+ @state() private mapSearchQuery = "";
createRenderRoot() {
return this;
@@ -233,6 +244,15 @@ export class GameConfigSettings extends LitElement {
);
}
+ private handleMapSearchInput = (event: Event) => {
+ const input = event.target as HTMLInputElement;
+ this.mapSearchQuery = input.value;
+ };
+
+ private clearMapSearch = () => {
+ this.mapSearchQuery = "";
+ };
+
private handleSelectMap = (map: GameMapType) => {
this.emit("map-selected", { map });
};
@@ -309,6 +329,42 @@ export class GameConfigSettings extends LitElement {
});
}
+ private renderMapSearchInput(): TemplateResult {
+ return html`
+
+
+ ${this.mapSearchQuery
+ ? html`
`
+ : null}
+
`;
+ }
+
render() {
if (!this.settings) return nothing;
const settings = this.settings;
@@ -328,7 +384,10 @@ export class GameConfigSettings extends LitElement {
.mapWins=${settings.map.mapWins ?? new Map()}
.onSelectMap=${this.handleSelectMap}
.onSelectRandom=${this.handleSelectRandom}
+ .searchQuery=${this.mapSearchQuery}
>`,
+ undefined,
+ this.renderMapSearchInput(),
)}
${renderSection(
DIFFICULTY_ICON,
diff --git a/src/client/components/map/MapPicker.ts b/src/client/components/map/MapPicker.ts
index 705ccef46..442b08f12 100644
--- a/src/client/components/map/MapPicker.ts
+++ b/src/client/components/map/MapPicker.ts
@@ -36,6 +36,7 @@ export class MapPicker extends LitElement {
@property({ type: Boolean }) useRandomMap = false;
@property({ type: Boolean }) showMedals = false;
@property({ type: Boolean }) randomMapDivider = false;
+ @property({ type: String }) searchQuery = "";
@property({ attribute: false }) mapWins: Map> =
new Map();
@property({ attribute: false }) onSelectMap?: (map: GameMapType) => void;
@@ -74,6 +75,16 @@ export class MapPicker extends LitElement {
event.preventDefault();
}
+ private get filteredMaps(): MapInfo[] {
+ if (!this.searchQuery.trim()) return [];
+ const query = this.searchQuery.trim().toLowerCase();
+ return maps.filter((m) => {
+ const name = translateText(m.translationKey).toLowerCase();
+ const id = m.id.toLowerCase();
+ return name.includes(query) || id.includes(query);
+ });
+ }
+
private getWins(mapValue: GameMapType): Set {
return this.mapWins?.get(mapValue) ?? new Set();
}
@@ -211,6 +222,36 @@ export class MapPicker extends LitElement {
}
}
+ private renderSearchResults() {
+ const results = this.filteredMaps;
+ if (results.length === 0) {
+ return html`
+
+
+ ${translateText("map_component.no_results")}
+
+
`;
+ }
+ return html`
+ ${this.renderSectionHeading(
+ `${translateText("map_component.search_results")} (${results.length})`,
+ )}
+ ${this.renderMapGrid(results)}
+
`;
+ }
+
private renderTabButton(tab: MapTab, label: string) {
const isActive = this.activeTab === tab;
return html`