feat: guide — barre de recherche live + lien sidebar (BookOpen, bas)
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 34s
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 34s
This commit is contained in:
@@ -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 */}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user