feat: add level search bar to world map
Search by level name, subtitle, ID, or world name. Shows filtered results as a flat grid with world.level numbering and world color. Escape key clears search, clear button resets and refocuses input. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<div className="gm-worldmap">
|
||||
@@ -69,8 +84,64 @@ export default function WorldMap({ onSelectLevel, onSandbox, onAdmin }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* All worlds */}
|
||||
{worlds.map((world, worldIdx) => {
|
||||
{/* Search bar */}
|
||||
<div className="gm-search-bar">
|
||||
<span className="gm-search-icon">🔍</span>
|
||||
<input
|
||||
ref={searchRef}
|
||||
className="gm-search-input"
|
||||
type="text"
|
||||
placeholder="Buscar nivel por nombre, mundo..."
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
onKeyDown={e => e.key === 'Escape' && (setSearch(''), searchRef.current?.blur())}
|
||||
/>
|
||||
{search && (
|
||||
<button className="gm-search-clear" onClick={() => { setSearch(''); searchRef.current?.focus(); }}>✕</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Search results */}
|
||||
{query ? (
|
||||
<div className="gm-search-results">
|
||||
{searchResults.length === 0 ? (
|
||||
<div className="gm-search-empty">No se encontraron niveles para "{search}"</div>
|
||||
) : (
|
||||
<div className="gm-search-count">{searchResults.length} nivel{searchResults.length !== 1 ? 'es' : ''} encontrado{searchResults.length !== 1 ? 's' : ''}</div>
|
||||
)}
|
||||
<div className="gm-level-grid">
|
||||
{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 (
|
||||
<div
|
||||
key={level.id}
|
||||
className={`gm-level-card ${levelUnlocked ? 'unlocked' : 'locked'} ${isBoss ? 'boss' : ''} ${stars === 3 ? 'perfect' : ''}`}
|
||||
onClick={() => levelUnlocked && onSelectLevel(level, world)}
|
||||
>
|
||||
<div className="gm-level-number" style={{ color: world.color }}>{worldIdx + 1}.{idx + 1}</div>
|
||||
<div className="gm-level-info">
|
||||
<h3 className="gm-level-title">{level.title}</h3>
|
||||
<p className="gm-level-subtitle">{world.name} — {level.subtitle}</p>
|
||||
</div>
|
||||
{levelUnlocked ? (
|
||||
<Stars count={stars} />
|
||||
) : (
|
||||
<span className="gm-lock">🔒</span>
|
||||
)}
|
||||
{!levelUnlocked && <div className="gm-lock-overlay" />}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
/* 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 }) {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user