diff --git a/src/game/WorldMap.jsx b/src/game/WorldMap.jsx index 04b1ed0..f34387e 100644 --- a/src/game/WorldMap.jsx +++ b/src/game/WorldMap.jsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState, useRef } from 'react'; import { WORLD_1 } from './levels/world1.js'; import { WORLD_2 } from './levels/world2.js'; import { WORLD_3 } from './levels/world3.js'; @@ -42,6 +42,21 @@ function isWorldUnlocked(world) { export default function WorldMap({ onSelectLevel, onSandbox, onAdmin }) { const totalStars = getTotalStars(); const maxStars = getMaxStars(); + const [search, setSearch] = useState(''); + const searchRef = useRef(null); + + const query = search.trim().toLowerCase(); + + // Build flat search results when there's a query + const searchResults = query ? worlds.flatMap((world, worldIdx) => { + return world.levels.map((level, idx) => ({ level, world, worldIdx, idx })) + .filter(({ level }) => + level.title.toLowerCase().includes(query) || + level.subtitle.toLowerCase().includes(query) || + level.id.toLowerCase().includes(query) || + world.name.toLowerCase().includes(query) + ); + }) : []; return (
@@ -69,8 +84,64 @@ export default function WorldMap({ onSelectLevel, onSandbox, onAdmin }) {
- {/* All worlds */} - {worlds.map((world, worldIdx) => { + {/* Search bar */} +
+ 🔍 + setSearch(e.target.value)} + onKeyDown={e => e.key === 'Escape' && (setSearch(''), searchRef.current?.blur())} + /> + {search && ( + + )} +
+ + {/* Search results */} + {query ? ( +
+ {searchResults.length === 0 ? ( +
No se encontraron niveles para "{search}"
+ ) : ( +
{searchResults.length} nivel{searchResults.length !== 1 ? 'es' : ''} encontrado{searchResults.length !== 1 ? 's' : ''}
+ )} +
+ {searchResults.map(({ level, world, worldIdx, idx }) => { + const progress = getLevelProgress(level.id); + const levelUnlocked = isLevelUnlocked(level.id, world.levels) && isWorldUnlocked(world); + const stars = progress?.stars || 0; + const isBoss = idx === world.levels.length - 1; + + return ( +
levelUnlocked && onSelectLevel(level, world)} + > +
{worldIdx + 1}.{idx + 1}
+
+

{level.title}

+

{world.name} — {level.subtitle}

+
+ {levelUnlocked ? ( + + ) : ( + 🔒 + )} + {!levelUnlocked &&
} +
+ ); + })} +
+
+ ) : ( + + /* All worlds (normal view) */ + worlds.map((world, worldIdx) => { const unlocked = isWorldUnlocked(world); const worldStars = world.levels.reduce((s, l) => { const p = getLevelProgress(l.id); @@ -136,7 +207,8 @@ export default function WorldMap({ onSelectLevel, onSandbox, onAdmin }) {
); - })} + }) + )} ); } diff --git a/src/index.css b/src/index.css index bf4a21e..568351d 100644 --- a/src/index.css +++ b/src/index.css @@ -323,6 +323,28 @@ html, body, #root { } .gm-sandbox-btn:hover { border-color: var(--accent); color: var(--text); } +/* Level search bar */ +.gm-search-bar { + display: flex; align-items: center; gap: 8px; + background: var(--surface); border: 1px solid var(--border); border-radius: 8px; + padding: 8px 14px; margin-bottom: 24px; transition: border-color 0.15s; +} +.gm-search-bar:focus-within { border-color: var(--accent); } +.gm-search-icon { font-size: 14px; opacity: 0.5; flex-shrink: 0; } +.gm-search-input { + flex: 1; background: none; border: none; outline: none; + color: var(--text); font-size: 13px; font-family: inherit; +} +.gm-search-input::placeholder { color: var(--text2); } +.gm-search-clear { + background: none; border: none; color: var(--text2); cursor: pointer; + font-size: 14px; padding: 2px 4px; border-radius: 4px; +} +.gm-search-clear:hover { color: var(--text); background: var(--surface2); } +.gm-search-results { margin-bottom: 24px; } +.gm-search-empty { color: var(--text2); font-size: 13px; padding: 16px 0; text-align: center; } +.gm-search-count { color: var(--text2); font-size: 11px; margin-bottom: 12px; } + /* World sections */ .gm-world-section { margin-bottom: 32px; } .gm-locked-world { opacity: 0.4; }