diff --git a/resources/lang/en.json b/resources/lang/en.json index 47eb4f2d7..cff6dcb7c 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -521,6 +521,7 @@ "map": "Map", "featured": "Featured", "all": "All", + "favorites": "Favourites", "world": "World", "worldinverted": "World Inverted", "giantworldmap": "Giant World Map", @@ -622,11 +623,15 @@ "tournament": "Tournament", "fantasy": "Other", "special": "Special", - "arcade": "Arcade" + "arcade": "Arcade", + "favorites": "Favourites" }, "map_component": { "loading": "Loading...", - "error": "Error" + "error": "Error", + "favorite": "Add to favourites", + "unfavorite": "Remove from favourites", + "favorites_empty": "Click the star on any map to favourite it" }, "private_lobby": { "title": "Join Private Lobby", diff --git a/src/client/components/map/MapDisplay.ts b/src/client/components/map/MapDisplay.ts index d86da025e..e600ca531 100644 --- a/src/client/components/map/MapDisplay.ts +++ b/src/client/components/map/MapDisplay.ts @@ -4,6 +4,7 @@ import medalIconRaw from "../../../../resources/images/MedalIconWhite.svg?raw"; import { Difficulty, GameMapType } from "../../../core/game/Game"; import { terrainMapFileLoader } from "../../TerrainMapFileLoader"; import { translateText } from "../../Utils"; +import { starIcon } from "./MapFavorites"; const medalMaskUrl = `url('data:image/svg+xml;utf8,${encodeURIComponent(medalIconRaw)}') no-repeat center / contain`; @@ -13,7 +14,9 @@ export class MapDisplay extends LitElement { @property({ type: Boolean }) selected = false; @property({ type: String }) translation: string = ""; @property({ type: Boolean }) showMedals = false; + @property({ type: Boolean }) favorite = false; @property({ attribute: false }) wins: Set = new Set(); + @property({ attribute: false }) onToggleFavorite?: () => void; @state() private mapWebpPath: string | null = null; @state() private mapName: string | null = null; @state() private isLoading = true; @@ -78,6 +81,34 @@ export class MapDisplay extends LitElement { event.preventDefault(); } + private handleToggleFavorite(event: Event) { + event.stopPropagation(); + event.preventDefault(); + this.onToggleFavorite?.(); + } + + private renderFavoriteButton() { + if (!this.onToggleFavorite) return null; + return html``; + } + render() { return html`
+ ${this.renderFavoriteButton()}
` : html`
(Object.values(GameMapType)); + +export function getFavoriteMaps(): GameMapType[] { + try { + const raw = localStorage.getItem(FAVORITES_KEY); + if (raw === null) return []; + const parsed = JSON.parse(raw); + if (!Array.isArray(parsed)) return []; + return parsed.filter( + (m): m is GameMapType => typeof m === "string" && validMaps.has(m), + ); + } catch (error) { + console.warn("Failed to read favorite maps from localStorage:", error); + return []; + } +} + +export function isFavoriteMap(map: GameMapType): boolean { + return getFavoriteMaps().includes(map); +} + +export function toggleFavoriteMap(map: GameMapType): GameMapType[] { + const favorites = getFavoriteMaps(); + const index = favorites.indexOf(map); + if (index >= 0) { + favorites.splice(index, 1); + } else { + favorites.push(map); + } + try { + localStorage.setItem(FAVORITES_KEY, JSON.stringify(favorites)); + } catch (error) { + console.warn("Failed to save favorite maps to localStorage:", error); + } + return favorites; +} + +export function starIcon(filled: boolean, className = ""): TemplateResult { + return html``; +} diff --git a/src/client/components/map/MapPicker.ts b/src/client/components/map/MapPicker.ts index ea1105337..7cafbc56c 100644 --- a/src/client/components/map/MapPicker.ts +++ b/src/client/components/map/MapPicker.ts @@ -8,8 +8,11 @@ import { } from "../../../core/game/Game"; import { translateText } from "../../Utils"; import "./MapDisplay"; +import { getFavoriteMaps, starIcon, toggleFavoriteMap } from "./MapFavorites"; const randomMap = assetUrl("images/RandomMap.webp"); +type MapTab = "featured" | "all" | "favorites"; + const featuredMaps: GameMapType[] = [ GameMapType.World, GameMapType.Europe, @@ -30,12 +33,17 @@ export class MapPicker extends LitElement { new Map(); @property({ attribute: false }) onSelectMap?: (map: GameMapType) => void; @property({ attribute: false }) onSelectRandom?: () => void; - @state() private showAllMaps = false; + @state() private activeTab: MapTab = "featured"; + @state() private favorites: GameMapType[] = getFavoriteMaps(); createRenderRoot() { return this; } + private handleToggleFavorite(mapValue: GameMapType) { + this.favorites = toggleFavoriteMap(mapValue); + } + private handleMapSelection(mapValue: GameMapType) { this.onSelectMap?.(mapValue); } @@ -66,6 +74,8 @@ export class MapPicker extends LitElement { .selected=${!this.useRandomMap && this.selectedMap === mapValue} .showMedals=${this.showMedals} .wins=${this.getWins(mapValue)} + .favorite=${this.favorites.includes(mapValue)} + .onToggleFavorite=${() => this.handleToggleFavorite(mapValue)} .translation=${translateText(`map.${mapKey?.toLowerCase()}`)} >
@@ -109,6 +119,55 @@ export class MapPicker extends LitElement { `; } + private renderFavoriteMaps() { + if (this.favorites.length === 0) { + return html`
+
${starIcon(false, "w-8 h-8")}
+

+ ${translateText("map_component.favorites_empty")} +

+
`; + } + return html`
+

+ ${translateText("map_categories.favorites")} +

+
+ ${this.favorites.map((mapValue) => this.renderMapCard(mapValue))} +
+
`; + } + + private renderActiveTab() { + switch (this.activeTab) { + case "all": + return this.renderAllMaps(); + case "favorites": + return this.renderFavoriteMaps(); + default: + return this.renderFeaturedMaps(); + } + } + + private renderTabButton(tab: MapTab, label: string) { + const isActive = this.activeTab === tab; + return html``; + } + render() { return html`
@@ -116,35 +175,14 @@ export class MapPicker extends LitElement {
- - + ${this.renderTabButton("featured", translateText("map.featured"))} + ${this.renderTabButton("all", translateText("map.all"))} + ${this.renderTabButton("favorites", translateText("map.favorites"))}
- ${this.showAllMaps ? this.renderAllMaps() : this.renderFeaturedMaps()} + ${this.renderActiveTab()}