All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 34s
- MonsterCard, CombatViews (Log+Multi+History), CreateCharacter - RarityBadge + RarityDot partagés (Guide, Drawer, pages) - CombatPage 341→215 lignes (−37%) - DashboardPage 368→307 lignes (−17%) - 9 composants dans components/
186 lines
8.0 KiB
TypeScript
186 lines
8.0 KiB
TypeScript
import { useState, useEffect, useRef } from 'react';
|
||
import { Search, X } from 'lucide-react';
|
||
import { useGuideData } from '../hooks/useGuideData';
|
||
import { RARITY_COLORS } from '../constants';
|
||
import { RarityDot } from './RarityBadge';
|
||
|
||
export function GuideDrawer({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||
const [search, setSearch] = useState('');
|
||
const inputRef = useRef<HTMLInputElement>(null);
|
||
const { filteredMonsters, filteredItems, filteredRecipes, matMap, q } = useGuideData(search);
|
||
|
||
useEffect(() => {
|
||
if (open) {
|
||
setSearch('');
|
||
setTimeout(() => inputRef.current?.focus(), 100);
|
||
}
|
||
}, [open]);
|
||
|
||
// Escape to close
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
|
||
window.addEventListener('keydown', handler);
|
||
return () => window.removeEventListener('keydown', handler);
|
||
}, [open, onClose]);
|
||
|
||
if (!open) return null;
|
||
|
||
const hasResults = q && (filteredMonsters.length > 0 || filteredItems.length > 0 || filteredRecipes.length > 0);
|
||
const noResults = q && !hasResults;
|
||
const totalResults = q ? filteredMonsters.length + filteredItems.length + filteredRecipes.length : 0;
|
||
|
||
return (
|
||
<>
|
||
{/* Backdrop */}
|
||
<div
|
||
onClick={onClose}
|
||
style={{
|
||
position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)',
|
||
zIndex: 90, transition: 'opacity 0.2s',
|
||
}}
|
||
/>
|
||
|
||
{/* Drawer */}
|
||
<div style={{
|
||
position: 'fixed', top: 0, right: 0, bottom: 0, width: 380,
|
||
background: '#0d0f14', borderLeft: '1px solid #2a3448',
|
||
zIndex: 100, display: 'flex', flexDirection: 'column',
|
||
animation: 'slideIn 0.2s ease-out',
|
||
}}>
|
||
{/* Header */}
|
||
<div style={{
|
||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||
padding: '12px 16px', borderBottom: '1px solid #2a3448',
|
||
}}>
|
||
<span style={{ fontWeight: 700, color: '#f4c94e', fontSize: 14 }}>📖 Guide rapide</span>
|
||
<button onClick={onClose} style={{ background: 'none', border: 'none', color: '#6b7a99', cursor: 'pointer', padding: 4 }}>
|
||
<X size={16} />
|
||
</button>
|
||
</div>
|
||
|
||
{/* Search */}
|
||
<div style={{ padding: '12px 16px', borderBottom: '1px solid #1e2535' }}>
|
||
<div style={{ position: 'relative' }}>
|
||
<Search size={13} style={{ position: 'absolute', left: 10, top: '50%', transform: 'translateY(-50%)', color: '#6b7a99' }} />
|
||
<input
|
||
ref={inputRef}
|
||
type="text"
|
||
value={search}
|
||
onChange={e => setSearch(e.target.value)}
|
||
placeholder="Monstre, item, matériau, recette…"
|
||
style={{
|
||
width: '100%', padding: '8px 10px 8px 30px', fontSize: 12,
|
||
background: '#1e2535', border: '1px solid #2a3448', borderRadius: 6,
|
||
color: '#dce4f0', outline: 'none', boxSizing: 'border-box',
|
||
}}
|
||
/>
|
||
</div>
|
||
{q && (
|
||
<div style={{ fontSize: 10, color: '#6b7a99', marginTop: 4 }}>
|
||
{totalResults} résultat{totalResults !== 1 ? 's' : ''}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Results */}
|
||
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 16px' }}>
|
||
{!q && (
|
||
<div style={{ textAlign: 'center', padding: '2rem 0', color: '#6b7a99', fontSize: 12 }}>
|
||
Tapez pour rechercher dans le guide
|
||
</div>
|
||
)}
|
||
|
||
{noResults && (
|
||
<div style={{ textAlign: 'center', padding: '2rem 0', color: '#6b7a99', fontSize: 12 }}>
|
||
Aucun résultat pour « {search} »
|
||
</div>
|
||
)}
|
||
|
||
{/* Monstres */}
|
||
{q && filteredMonsters.length > 0 && (
|
||
<div style={{ marginBottom: 16 }}>
|
||
<div style={{ fontSize: 10, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase', marginBottom: 6 }}>
|
||
Monstres ({filteredMonsters.length})
|
||
</div>
|
||
{filteredMonsters.map(m => (
|
||
<div key={m.id} style={{ padding: '6px 0', borderBottom: '1px solid #1e2535', fontSize: 12 }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
|
||
<span style={{ color: '#dce4f0', fontWeight: 600 }}>{m.name}</span>
|
||
<span style={{ color: '#6b7a99', fontSize: 10 }}>Niv. {m.minLevel}–{m.maxLevel}</span>
|
||
</div>
|
||
<div style={{ color: '#6b7a99', fontSize: 10, marginTop: 2 }}>
|
||
❤️{m.hp} ⚔️{m.attack} 🛡️{m.defense} · ⭐{m.xpReward}xp · 💰{m.goldMin}–{m.goldMax}
|
||
{m.dropMaterialId && matMap.get(m.dropMaterialId) && (
|
||
<span style={{ color: '#f4c94e' }}> · 🎁 {matMap.get(m.dropMaterialId)!.name}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Items */}
|
||
{q && filteredItems.length > 0 && (
|
||
<div style={{ marginBottom: 16 }}>
|
||
<div style={{ fontSize: 10, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase', marginBottom: 6 }}>
|
||
Équipement ({filteredItems.length})
|
||
</div>
|
||
{filteredItems.map(item => (
|
||
<div key={item.id} style={{ padding: '6px 0', borderBottom: '1px solid #1e2535', fontSize: 12 }}>
|
||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
<span style={{ color: RARITY_COLORS[item.rarity], fontWeight: 600 }}>
|
||
<RarityDot rarity={item.rarity} />
|
||
{item.type === 'weapon' ? '⚔️' : item.type === 'armor' ? '🛡️' : '🧪'} {item.name}
|
||
</span>
|
||
</div>
|
||
<div style={{ color: '#6b7a99', fontSize: 10, marginTop: 2 }}>
|
||
{item.attackBonus > 0 && `ATK+${item.attackBonus} `}
|
||
{item.defenseBonus > 0 && `DEF+${item.defenseBonus} `}
|
||
{item.agiliteBonus > 0 && `AGI+${item.agiliteBonus} `}
|
||
{item.intelligenceBonus > 0 && `INT+${item.intelligenceBonus} `}
|
||
{(item as any).buyPrice > 0 ? `· 💰${(item as any).buyPrice}` : '· 🔨 Craft'}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
|
||
{/* Recettes */}
|
||
{q && filteredRecipes.length > 0 && (
|
||
<div style={{ marginBottom: 16 }}>
|
||
<div style={{ fontSize: 10, fontWeight: 700, color: '#6b7a99', textTransform: 'uppercase', marginBottom: 6 }}>
|
||
Recettes ({filteredRecipes.length})
|
||
</div>
|
||
{filteredRecipes.map(r => (
|
||
<div key={r.id} style={{ padding: '6px 0', borderBottom: '1px solid #1e2535', fontSize: 12 }}>
|
||
<div style={{ color: RARITY_COLORS[r.resultItem?.rarity] ?? '#dce4f0', fontWeight: 600 }}>
|
||
<RarityDot rarity={r.resultItem?.rarity ?? 'common'} />
|
||
{r.resultItem?.name ?? r.name}
|
||
</div>
|
||
<div style={{ color: '#6b7a99', fontSize: 10, marginTop: 2 }}>
|
||
{r.ingredients.map((ing, i) => (
|
||
<span key={i}>{i > 0 ? ' + ' : ''}{ing.quantity}× {matMap.get(ing.materialId)?.name ?? '???'}</span>
|
||
))}
|
||
<span> · ⏱️{r.craftDurationSeconds}s · ⚡{r.enduranceCost}</span>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Footer */}
|
||
<div style={{
|
||
padding: '10px 16px', borderTop: '1px solid #2a3448',
|
||
textAlign: 'center',
|
||
}}>
|
||
<a href="/guide" style={{ fontSize: 11, color: '#6b7a99', textDecoration: 'none' }}>
|
||
Ouvrir le guide complet →
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|