feat: guide — barre de recherche live + lien sidebar (BookOpen, bas)
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 34s

This commit is contained in:
2026-03-24 21:25:30 +01:00
parent 823d7911f0
commit 84104cd96f
2 changed files with 62 additions and 7 deletions

View File

@@ -1,6 +1,6 @@
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import { Swords, Package, Hammer, User, LogOut, Shield, Scroll, Trophy, ShoppingBag } from 'lucide-react'; import { Swords, Package, Hammer, User, LogOut, Shield, Scroll, Trophy, ShoppingBag, BookOpen } from 'lucide-react';
import { HudBar } from './HudBar'; import { HudBar } from './HudBar';
const NAV = [ const NAV = [
@@ -83,6 +83,22 @@ export function Layout({ children }: { children: React.ReactNode }) {
</Link> </Link>
); );
})} })}
<div style={{ flex: 1 }} />
<Link to="/guide" title="Guide" style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 40,
height: 40,
borderRadius: 8,
color: loc.pathname === '/guide' ? '#f4c94e' : '#6b7a99',
background: loc.pathname === '/guide' ? '#1e2535' : 'transparent',
border: loc.pathname === '/guide' ? '1px solid #c49c2e' : '1px solid transparent',
textDecoration: 'none',
transition: 'all 0.15s',
}}>
<BookOpen size={18} />
</Link>
</nav> </nav>
{/* Main content */} {/* Main content */}

View File

@@ -1,8 +1,8 @@
import { useState } from 'react'; import { useState, useMemo } from 'react';
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { api } from '../api/client'; import { api } from '../api/client';
import type { Monster, Item, Recipe } from '../api/types'; import type { Monster, Item, Recipe } from '../api/types';
import { Swords, Shield, Map as MapIcon, Hammer, ShoppingBag, BookOpen, Sparkles } from 'lucide-react'; import { Swords, Shield, Map as MapIcon, Hammer, ShoppingBag, BookOpen, Sparkles, Search } from 'lucide-react';
// ── Data fetching (public endpoints, no auth) ── // ── Data fetching (public endpoints, no auth) ──
@@ -419,12 +419,26 @@ function ShopTab({ items }: { items: Item[] }) {
export function GuidePage() { export function GuidePage() {
const [tab, setTab] = useState('start'); const [tab, setTab] = useState('start');
const [search, setSearch] = useState('');
const { data: monsters = [] } = useQuery({ queryKey: ['guide-monsters'], queryFn: guideApi.monsters }); const { data: monsters = [] } = useQuery({ queryKey: ['guide-monsters'], queryFn: guideApi.monsters });
const { data: items = [] } = useQuery({ queryKey: ['guide-items'], queryFn: guideApi.items }); const { data: items = [] } = useQuery({ queryKey: ['guide-items'], queryFn: guideApi.items });
const { data: materials = [] } = useQuery({ queryKey: ['guide-materials'], queryFn: guideApi.materials }); const { data: materials = [] } = useQuery({ queryKey: ['guide-materials'], queryFn: guideApi.materials });
const { data: recipes = [] } = useQuery({ queryKey: ['guide-recipes'], queryFn: guideApi.recipes }); const { data: recipes = [] } = useQuery({ queryKey: ['guide-recipes'], queryFn: guideApi.recipes });
const q = search.toLowerCase().trim();
const filteredMonsters = useMemo(() => q ? monsters.filter(m => m.name.toLowerCase().includes(q) || m.zone?.toLowerCase().includes(q)) : monsters, [monsters, q]);
const filteredItems = useMemo(() => q ? items.filter(i => i.name.toLowerCase().includes(q) || i.rarity.toLowerCase().includes(q) || i.description?.toLowerCase().includes(q)) : items, [items, q]);
const filteredRecipes = useMemo(() => {
if (!q) return recipes;
const matMap = new Map<string, any>(materials.map(m => [m.id, m]));
return recipes.filter(r =>
r.name.toLowerCase().includes(q) ||
r.resultItem?.name.toLowerCase().includes(q) ||
r.ingredients.some(ing => matMap.get(ing.materialId)?.name.toLowerCase().includes(q))
);
}, [recipes, materials, q]);
return ( return (
<div style={{ maxWidth: 900, margin: '0 auto', padding: '2rem 1rem' }}> <div style={{ maxWidth: 900, margin: '0 auto', padding: '2rem 1rem' }}>
{/* Header */} {/* Header */}
@@ -437,6 +451,31 @@ export function GuidePage() {
</p> </p>
</div> </div>
{/* Search */}
<div style={{ position: 'relative', marginBottom: '1rem' }}>
<Search size={14} style={{ position: 'absolute', left: 12, top: '50%', transform: 'translateY(-50%)', color: '#6b7a99' }} />
<input
type="text"
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Rechercher un monstre, item, matériau, recette…"
style={{
width: '100%', padding: '10px 12px 10px 34px', fontSize: 13,
background: '#1e2535', border: '1px solid #2a3448', borderRadius: 8,
color: '#dce4f0', outline: 'none', boxSizing: 'border-box',
}}
/>
{search && (
<button
onClick={() => setSearch('')}
style={{
position: 'absolute', right: 10, top: '50%', transform: 'translateY(-50%)',
background: 'none', border: 'none', color: '#6b7a99', cursor: 'pointer', fontSize: 14,
}}
>✕</button>
)}
</div>
{/* Tab navigation */} {/* Tab navigation */}
<div style={{ <div style={{
display: 'flex', gap: 4, marginBottom: '1.5rem', overflowX: 'auto', display: 'flex', gap: 4, marginBottom: '1.5rem', overflowX: 'auto',
@@ -469,11 +508,11 @@ export function GuidePage() {
{/* Tab content */} {/* Tab content */}
{tab === 'start' && <StartTab />} {tab === 'start' && <StartTab />}
{tab === 'zones' && <ZonesTab />} {tab === 'zones' && <ZonesTab />}
{tab === 'bestiary' && <BestiaryTab monsters={monsters} materials={materials} />} {tab === 'bestiary' && <BestiaryTab monsters={filteredMonsters} materials={materials} />}
{tab === 'items' && <ItemsTab items={items} />} {tab === 'items' && <ItemsTab items={filteredItems} />}
{tab === 'craft' && <CraftTab recipes={recipes} materials={materials} />} {tab === 'craft' && <CraftTab recipes={filteredRecipes} materials={materials} />}
{tab === 'forge' && <ForgeTab />} {tab === 'forge' && <ForgeTab />}
{tab === 'shop' && <ShopTab items={items} />} {tab === 'shop' && <ShopTab items={filteredItems} />}
</div> </div>
); );
} }