feat: page Guide publique — wiki joueur dynamique
All checks were successful
CI/CD — Build & Deploy / Build & Deploy (push) Successful in 33s

- /guide accessible sans authentification
- 7 onglets : Démarrer, Zones, Bestiaire, Équipement, Artisanat, Forge, Boutique
- Données dynamiques (API publiques) — toujours à jour
- Endpoint /monsters/bestiary public (bestiaire complet toutes zones)
- Fix Item.type → inclut 'consumable'
This commit is contained in:
2026-03-24 21:19:08 +01:00
parent 4fc8be9ea0
commit 823d7911f0
4 changed files with 490 additions and 2 deletions

View File

@@ -12,6 +12,7 @@ import { ForgePage } from './pages/ForgePage';
import { QuestPage } from './pages/QuestPage'; import { QuestPage } from './pages/QuestPage';
import { AchievementsPage } from './pages/AchievementsPage'; import { AchievementsPage } from './pages/AchievementsPage';
import { ShopPage } from './pages/ShopPage'; import { ShopPage } from './pages/ShopPage';
import { GuidePage } from './pages/GuidePage';
const qc = new QueryClient({ defaultOptions: { queries: { retry: 1, staleTime: 30_000 } } }); const qc = new QueryClient({ defaultOptions: { queries: { retry: 1, staleTime: 30_000 } } });
@@ -31,6 +32,7 @@ function AppRoutes() {
<Routes> <Routes>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/auth/callback" element={<AuthCallback />} /> <Route path="/auth/callback" element={<AuthCallback />} />
<Route path="/guide" element={<GuidePage />} />
<Route path="/dashboard" element={<ProtectedLayout><DashboardPage /></ProtectedLayout>} /> <Route path="/dashboard" element={<ProtectedLayout><DashboardPage /></ProtectedLayout>} />
<Route path="/quests" element={<ProtectedLayout><QuestPage /></ProtectedLayout>} /> <Route path="/quests" element={<ProtectedLayout><QuestPage /></ProtectedLayout>} />
<Route path="/combat" element={<ProtectedLayout><CombatPage /></ProtectedLayout>} /> <Route path="/combat" element={<ProtectedLayout><CombatPage /></ProtectedLayout>} />

View File

@@ -119,7 +119,7 @@ export interface Item {
id: string; id: string;
name: string; name: string;
description: string; description: string;
type: 'weapon' | 'armor'; type: 'weapon' | 'armor' | 'consumable';
rarity: Rarity; rarity: Rarity;
attackBonus: number; attackBonus: number;
defenseBonus: number; defenseBonus: number;

View File

@@ -0,0 +1,479 @@
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { api } from '../api/client';
import type { Monster, Item, Recipe } from '../api/types';
import { Swords, Shield, Map as MapIcon, Hammer, ShoppingBag, BookOpen, Sparkles } from 'lucide-react';
// ── Data fetching (public endpoints, no auth) ──
const guideApi = {
monsters: () => api.get<(Monster & { zone: string })[]>('/monsters/bestiary'),
items: () => api.get<Item[]>('/items'),
materials: () => api.get<any[]>('/materials'),
recipes: () => api.get<Recipe[]>('/craft/recipes'),
};
// ── Constants ──
const ZONES = [
{ id: 'marais', name: 'Les Marais', emoji: '🌿', desc: 'Zone de départ. Monstres niv. 1-9. Terre de boue et de brume.', color: '#3ddc84' },
{ id: 'egouts', name: 'Les Égouts', emoji: '🕳️', desc: 'Sous-terrain infesté. Monstres niv. 4-10. Rats, slimes et croco.', color: '#5ba4f5' },
{ id: 'desert', name: 'Le Désert', emoji: '🏜️', desc: 'Sable brûlant. Monstres niv. 8-15. Scorpions, momies et le Sphinx.', color: '#f4c94e' },
];
const RARITY_COLORS: Record<string, string> = {
common: '#9ca3af', rare: '#5ba4f5', epic: '#a78bfa', legendary: '#f4c94e',
};
const RARITY_LABELS: Record<string, string> = {
common: 'Commun', rare: 'Rare', epic: 'Épique', legendary: 'Légendaire',
};
const FORGE_TABLE = [
{ level: 1, gold: 50, endurance: 10, risk: '0%', bonus: '+2' },
{ level: 2, gold: 100, endurance: 10, risk: '0%', bonus: '+4' },
{ level: 3, gold: 250, endurance: 10, risk: '20%', bonus: '+6' },
{ level: 4, gold: 500, endurance: 10, risk: '30%', bonus: '+8' },
{ level: 5, gold: 1000, endurance: 10, risk: '40%', bonus: '+10' },
];
const TABS = [
{ id: 'start', label: 'Démarrer', icon: BookOpen },
{ id: 'zones', label: 'Zones', icon: MapIcon },
{ id: 'bestiary', label: 'Bestiaire', icon: Swords },
{ id: 'items', label: 'Équipement', icon: Shield },
{ id: 'craft', label: 'Artisanat', icon: Hammer },
{ id: 'forge', label: 'Forge', icon: Sparkles },
{ id: 'shop', label: 'Boutique', icon: ShoppingBag },
];
// ── Components ──
function RarityBadge({ rarity }: { rarity: string }) {
return (
<span style={{
fontSize: 10, fontWeight: 700, padding: '2px 6px', borderRadius: 4,
background: RARITY_COLORS[rarity] + '22', color: RARITY_COLORS[rarity],
}}>
{RARITY_LABELS[rarity] ?? rarity}
</span>
);
}
function StatBar({ label, value, max, color }: { label: string; value: number; max: number; color: string }) {
return (
<div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 11 }}>
<span style={{ width: 24, color: '#6b7a99' }}>{label}</span>
<div style={{ flex: 1, height: 6, background: '#1e2535', borderRadius: 3 }}>
<div style={{ width: `${Math.min(100, (value / max) * 100)}%`, height: '100%', background: color, borderRadius: 3 }} />
</div>
<span style={{ width: 28, textAlign: 'right', color: '#dce4f0' }}>{value}</span>
</div>
);
}
// ── Tab: Démarrer ──
function StartTab() {
return (
<div>
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Bienvenue dans TetaRdPG</h3>
<div className="card" style={{ marginBottom: '1rem' }}>
<p style={{ color: '#dce4f0', fontSize: 13, lineHeight: 1.6, margin: 0 }}>
TetaRdPG est un RPG textuel idle chaque action coûte de l'endurance.
Combattez des monstres, récoltez des matériaux, craftez de l'équipement et forgez-le pour devenir plus fort.
</p>
</div>
<h4 style={{ color: '#dce4f0', margin: '1rem 0 0.5rem', fontSize: 14 }}>Comment progresser ?</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
{[
{ step: '1', title: 'Combattre', desc: 'Affrontez des monstres pour gagner XP, Or et matériaux.' },
{ step: '2', title: 'Récolter', desc: 'Chaque monstre peut dropper un matériau propre à sa zone.' },
{ step: '3', title: 'Crafter', desc: 'Utilisez vos matériaux pour fabriquer des armes et armures.' },
{ step: '4', title: 'Forger', desc: 'Améliorez votre équipement (+1 à +5) pour des bonus de stats.' },
].map(s => (
<div key={s.step} className="card" style={{ padding: '0.75rem' }}>
<div style={{ fontSize: 20, color: '#f4c94e', fontWeight: 800 }}>{s.step}</div>
<div style={{ fontSize: 13, fontWeight: 700, color: '#dce4f0', marginBottom: 4 }}>{s.title}</div>
<div style={{ fontSize: 11, color: '#6b7a99' }}>{s.desc}</div>
</div>
))}
</div>
<h4 style={{ color: '#dce4f0', margin: '1rem 0 0.5rem', fontSize: 14 }}>Mécaniques clés</h4>
<div className="card" style={{ fontSize: 12, lineHeight: 1.8, color: '#9ca3af' }}>
<p style={{ margin: '0 0 4px' }}><strong style={{ color: '#dce4f0' }}>Endurance</strong> Chaque combat coûte 5. Recharge : 1 point / 6 min. Max : 100+.</p>
<p style={{ margin: '0 0 4px' }}><strong style={{ color: '#dce4f0' }}>Types d'attaque</strong> — Mêlée (Force ×1.5), Distance (Agilité ×1.5), Magie (Intelligence ×1.5).</p>
<p style={{ margin: '0 0 4px' }}><strong style={{ color: '#dce4f0' }}>Drop rate</strong> — Varie selon la difficulté du monstre : 25% (facile) → 80% (boss). Les boss droppent 2-3 matériaux.</p>
<p style={{ margin: 0 }}><strong style={{ color: '#dce4f0' }}>Défaite</strong> — Perte d'endurance (25), PV réduits à 20%, perte de 5% de l'or.</p>
</div>
</div>
);
}
// ── Tab: Zones ──
function ZonesTab() {
return (
<div>
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Les Zones</h3>
<p style={{ color: '#6b7a99', fontSize: 12, marginBottom: '1rem' }}>
Progressez de zone en zone en complétant les arcs de quêtes. Chaque zone a ses monstres, matériaux et équipements.
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{ZONES.map((z, i) => (
<div key={z.id} className="card" style={{ borderLeft: `3px solid ${z.color}` }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
<span style={{ fontSize: 24 }}>{z.emoji}</span>
<div>
<div style={{ fontWeight: 700, color: z.color, fontSize: 15 }}>{z.name}</div>
<div style={{ fontSize: 11, color: '#6b7a99' }}>{z.desc}</div>
</div>
</div>
<div style={{ fontSize: 11, color: '#9ca3af' }}>
{i === 0 ? '🔓 Toujours accessible' : `🔒 Déblocage : compléter l'arc de quêtes ${ZONES[i - 1].name}`}
</div>
</div>
))}
</div>
</div>
);
}
// ── Tab: Bestiaire ──
function BestiaryTab({ monsters, materials }: { monsters: (Monster & { zone: string })[]; materials: any[] }) {
const matMap = new Map<string, any>(materials.map(m => [m.id, m]));
return (
<div>
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Bestiaire</h3>
{ZONES.map(zone => {
const zoneMonsters = monsters.filter(m => m.zone === zone.id).sort((a, b) => a.minLevel - b.minLevel);
if (!zoneMonsters.length) return null;
return (
<div key={zone.id} style={{ marginBottom: '1.5rem' }}>
<p style={{ margin: '0 0 0.5rem', fontSize: 13, fontWeight: 700, color: zone.color }}>
{zone.emoji} {zone.name}
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
{zoneMonsters.map(m => {
const dropMat = m.dropMaterialId ? matMap.get(m.dropMaterialId) : null;
return (
<div key={m.id} className="card" style={{ padding: '0.75rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
<span style={{ fontWeight: 700, color: '#dce4f0', fontSize: 13 }}>{m.name}</span>
<span className="badge badge-green" style={{ fontSize: 10 }}>Niv. {m.minLevel}{m.maxLevel}</span>
</div>
<div style={{ display: 'flex', gap: 4, marginBottom: 6 }}>
<StatBar label="PV" value={m.hp} max={300} color="#e84040" />
</div>
<div style={{ display: 'flex', gap: 16, fontSize: 11, color: '#6b7a99' }}>
<span>⚔️ {m.attack}</span>
<span>🛡️ {m.defense}</span>
<span>⭐ {m.xpReward} XP</span>
<span>💰 {m.goldMin}{m.goldMax}</span>
<span style={{ textTransform: 'capitalize' }}>🎯 {m.attackType}</span>
</div>
{dropMat && (
<div style={{ marginTop: 6, fontSize: 11, color: '#f4c94e' }}>
🎁 Drop : {dropMat.name} <RarityBadge rarity={dropMat.rarity} />
</div>
)}
</div>
);
})}
</div>
</div>
);
})}
</div>
);
}
// ── Tab: Équipement ──
function ItemsTab({ items }: { items: Item[] }) {
const equipment = items.filter(i => i.type !== 'consumable');
const consumables = items.filter(i => i.type === 'consumable');
return (
<div>
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Équipement</h3>
{ZONES.map(zone => {
const zoneItems = equipment.filter(i => (i as any).zone === zone.id).sort((a, b) => a.attackBonus + a.defenseBonus - b.attackBonus - b.defenseBonus);
if (!zoneItems.length) return null;
return (
<div key={zone.id} style={{ marginBottom: '1.5rem' }}>
<p style={{ margin: '0 0 0.5rem', fontSize: 13, fontWeight: 700, color: zone.color }}>
{zone.emoji} {zone.name}
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
{zoneItems.map(item => (
<div key={item.id} className="card" style={{ padding: '0.75rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontWeight: 700, color: RARITY_COLORS[item.rarity], fontSize: 12 }}>
{item.type === 'weapon' ? '⚔️' : '🛡️'} {item.name}
</span>
<RarityBadge rarity={item.rarity} />
</div>
<div style={{ fontSize: 11, color: '#6b7a99', marginBottom: 4 }}>{item.description}</div>
<div style={{ fontSize: 11, color: '#dce4f0' }}>
{item.attackBonus > 0 && <span>ATK +{item.attackBonus} </span>}
{item.defenseBonus > 0 && <span>DEF +{item.defenseBonus} </span>}
{item.agiliteBonus > 0 && <span>AGI +{item.agiliteBonus} </span>}
{item.intelligenceBonus > 0 && <span>INT +{item.intelligenceBonus} </span>}
{(item as any).buyPrice > 0 && <span style={{ color: '#f4c94e' }}>💰 {(item as any).buyPrice}</span>}
{(item as any).buyPrice === 0 && <span style={{ color: '#a78bfa' }}>🔨 Craft only</span>}
</div>
</div>
))}
</div>
</div>
);
})}
{consumables.length > 0 && (
<div style={{ marginBottom: '1.5rem' }}>
<p style={{ margin: '0 0 0.5rem', fontSize: 13, fontWeight: 700, color: '#e84040' }}>🧪 Consommables</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
{consumables.map(item => (
<div key={item.id} className="card" style={{ padding: '0.75rem' }}>
<div style={{ fontWeight: 700, color: '#dce4f0', fontSize: 12, marginBottom: 4 }}>
🧪 {item.name}
</div>
<div style={{ fontSize: 11, color: '#6b7a99' }}>{item.description}</div>
{(item as any).buyPrice > 0 && (
<div style={{ fontSize: 11, color: '#f4c94e', marginTop: 4 }}>💰 {(item as any).buyPrice}</div>
)}
</div>
))}
</div>
</div>
)}
</div>
);
}
// ── Tab: Artisanat ──
function CraftTab({ recipes, materials }: { recipes: Recipe[]; materials: any[] }) {
const matMap = new Map<string, any>(materials.map(m => [m.id, m]));
return (
<div>
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Artisanat</h3>
<p style={{ color: '#6b7a99', fontSize: 12, marginBottom: '1rem' }}>
Combinez des matériaux droppés par les monstres pour crafter de l'équipement unique — souvent meilleur que la boutique.
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{recipes.sort((a, b) => a.enduranceCost - b.enduranceCost).map(recipe => (
<div key={recipe.id} className="card" style={{ padding: '0.75rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
<span style={{ fontWeight: 700, color: RARITY_COLORS[recipe.resultItem?.rarity] ?? '#dce4f0', fontSize: 13 }}>
{recipe.resultItem?.type === 'weapon' ? '⚔️' : recipe.resultItem?.type === 'armor' ? '🛡️' : '🧪'} {recipe.resultItem?.name ?? recipe.name}
</span>
{recipe.resultItem && <RarityBadge rarity={recipe.resultItem.rarity} />}
</div>
{recipe.resultItem && (
<div style={{ fontSize: 11, color: '#dce4f0', marginBottom: 6 }}>
{recipe.resultItem.attackBonus > 0 && <span>ATK +{recipe.resultItem.attackBonus} </span>}
{recipe.resultItem.defenseBonus > 0 && <span>DEF +{recipe.resultItem.defenseBonus} </span>}
{recipe.resultItem.agiliteBonus > 0 && <span>AGI +{recipe.resultItem.agiliteBonus} </span>}
{recipe.resultItem.intelligenceBonus > 0 && <span>INT +{recipe.resultItem.intelligenceBonus} </span>}
</div>
)}
<div style={{ fontSize: 11, color: '#9ca3af', marginBottom: 6 }}>
<span>⏱️ {recipe.craftDurationSeconds}s</span>
<span style={{ marginLeft: 12 }}>⚡ {recipe.enduranceCost} endurance</span>
</div>
<div style={{ fontSize: 11, color: '#6b7a99' }}>
Ingrédients : {recipe.ingredients.map((ing, i) => {
const mat = matMap.get(ing.materialId);
return (
<span key={i}>
{i > 0 && ' + '}
<span style={{ color: RARITY_COLORS[mat?.rarity] ?? '#dce4f0' }}>
{ing.quantity}× {mat?.name ?? '???'}
</span>
</span>
);
})}
</div>
</div>
))}
</div>
</div>
);
}
// ── Tab: Forge ──
function ForgeTab() {
return (
<div>
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Forge</h3>
<p style={{ color: '#6b7a99', fontSize: 12, marginBottom: '1rem' }}>
Améliorez vos armes et armures de +1 à +5. Les niveaux 1-2 sont garantis. A partir du +3, il y a un risque d'échec — l'or et l'endurance sont perdus même en cas d'échec.
</p>
<div className="card">
<table style={{ width: '100%', fontSize: 12, borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #2a3448', color: '#6b7a99', fontSize: 11 }}>
<th style={{ padding: '8px 4px', textAlign: 'left' }}>Niveau</th>
<th style={{ padding: '8px 4px', textAlign: 'right' }}>Or</th>
<th style={{ padding: '8px 4px', textAlign: 'right' }}>Endurance</th>
<th style={{ padding: '8px 4px', textAlign: 'right' }}>Risque</th>
<th style={{ padding: '8px 4px', textAlign: 'right' }}>Bonus total</th>
</tr>
</thead>
<tbody>
{FORGE_TABLE.map(row => (
<tr key={row.level} style={{ borderBottom: '1px solid #1e2535' }}>
<td style={{ padding: '6px 4px', color: '#f4c94e', fontWeight: 700 }}>+{row.level}</td>
<td style={{ padding: '6px 4px', textAlign: 'right', color: '#dce4f0' }}>{row.gold} 💰</td>
<td style={{ padding: '6px 4px', textAlign: 'right', color: '#5ba4f5' }}>{row.endurance} ⚡</td>
<td style={{ padding: '6px 4px', textAlign: 'right', color: row.risk === '0%' ? '#3ddc84' : '#e84040' }}>{row.risk}</td>
<td style={{ padding: '6px 4px', textAlign: 'right', color: '#a78bfa', fontWeight: 700 }}>{row.bonus}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="card" style={{ marginTop: '0.75rem', fontSize: 12, color: '#9ca3af', lineHeight: 1.6 }}>
<p style={{ margin: '0 0 4px' }}><strong style={{ color: '#3ddc84' }}>Niv. 1-2</strong> — Succès garanti. Aucun risque.</p>
<p style={{ margin: '0 0 4px' }}><strong style={{ color: '#e84040' }}>Niv. 3-5</strong> — Risque croissant. En cas d'échec : or et endurance perdus, équipement intact.</p>
<p style={{ margin: 0 }}><strong style={{ color: '#f4c94e' }}>Bonus</strong> — +2 ATK (armes) ou +2 DEF (armures) par niveau de forge.</p>
</div>
</div>
);
}
// ── Tab: Boutique ──
function ShopTab({ items }: { items: Item[] }) {
const shopItems = items.filter(i => (i as any).buyPrice > 0);
return (
<div>
<h3 style={{ color: '#f4c94e', margin: '0 0 1rem', fontSize: 18 }}>Boutique</h3>
<p style={{ color: '#6b7a99', fontSize: 12, marginBottom: '1rem' }}>
Achetez de l'équipement avec votre or. Les items de la boutique sont accessibles dès que vous débloquez la zone correspondante.
Revente : 40% du prix + 50% de l'investissement de forge.
</p>
{ZONES.map(zone => {
const zoneShop = shopItems.filter(i => (i as any).zone === zone.id).sort((a, b) => (a as any).buyPrice - (b as any).buyPrice);
if (!zoneShop.length) return null;
return (
<div key={zone.id} style={{ marginBottom: '1.5rem' }}>
<p style={{ margin: '0 0 0.5rem', fontSize: 13, fontWeight: 700, color: zone.color }}>
{zone.emoji} {zone.name}
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
{zoneShop.map(item => (
<div key={item.id} className="card" style={{ padding: '0.75rem' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<span style={{ fontWeight: 700, color: '#dce4f0', fontSize: 12 }}>
{item.type === 'weapon' ? '⚔️' : item.type === 'armor' ? '🛡️' : '🧪'} {item.name}
</span>
<span style={{ color: '#f4c94e', fontSize: 12, fontWeight: 700 }}>{(item as any).buyPrice} 💰</span>
</div>
<div style={{ fontSize: 11, color: '#6b7a99' }}>
{item.attackBonus > 0 && <span>ATK +{item.attackBonus} </span>}
{item.defenseBonus > 0 && <span>DEF +{item.defenseBonus} </span>}
<span> · Niv. {(item as any).minLevel}+</span>
</div>
</div>
))}
</div>
</div>
);
})}
{/* Potions */}
{(() => {
const potions = shopItems.filter(i => i.type === 'consumable');
if (!potions.length) return null;
return (
<div>
<p style={{ margin: '0 0 0.5rem', fontSize: 13, fontWeight: 700, color: '#e84040' }}>🧪 Potions</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
{potions.map(item => (
<div key={item.id} className="card" style={{ padding: '0.75rem' }}>
<div style={{ fontWeight: 700, color: '#dce4f0', fontSize: 12, marginBottom: 4 }}>{item.name}</div>
<div style={{ fontSize: 11, color: '#6b7a99' }}>{item.description}</div>
<div style={{ fontSize: 11, color: '#f4c94e', marginTop: 4 }}>{(item as any).buyPrice} 💰</div>
</div>
))}
</div>
</div>
);
})()}
</div>
);
}
// ── Main Page ──
export function GuidePage() {
const [tab, setTab] = useState('start');
const { data: monsters = [] } = useQuery({ queryKey: ['guide-monsters'], queryFn: guideApi.monsters });
const { data: items = [] } = useQuery({ queryKey: ['guide-items'], queryFn: guideApi.items });
const { data: materials = [] } = useQuery({ queryKey: ['guide-materials'], queryFn: guideApi.materials });
const { data: recipes = [] } = useQuery({ queryKey: ['guide-recipes'], queryFn: guideApi.recipes });
return (
<div style={{ maxWidth: 900, margin: '0 auto', padding: '2rem 1rem' }}>
{/* Header */}
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
<h1 style={{ color: '#f4c94e', fontSize: 28, margin: '0 0 0.25rem', fontWeight: 800 }}>
📖 Guide du Têtard
</h1>
<p style={{ color: '#6b7a99', fontSize: 13, margin: 0 }}>
Tout ce qu'il faut savoir pour survivre dans le monde de TetaRdPG
</p>
</div>
{/* Tab navigation */}
<div style={{
display: 'flex', gap: 4, marginBottom: '1.5rem', overflowX: 'auto',
borderBottom: '1px solid #2a3448', paddingBottom: 8,
}}>
{TABS.map(t => {
const Icon = t.icon;
const active = tab === t.id;
return (
<button
key={t.id}
onClick={() => setTab(t.id)}
style={{
display: 'flex', alignItems: 'center', gap: 6,
padding: '8px 14px', border: 'none', borderRadius: 6,
background: active ? '#f4c94e22' : 'transparent',
color: active ? '#f4c94e' : '#6b7a99',
fontWeight: active ? 700 : 500, fontSize: 12,
cursor: 'pointer', whiteSpace: 'nowrap',
transition: 'all 0.15s',
}}
>
<Icon size={14} />
{t.label}
</button>
);
})}
</div>
{/* Tab content */}
{tab === 'start' && <StartTab />}
{tab === 'zones' && <ZonesTab />}
{tab === 'bestiary' && <BestiaryTab monsters={monsters} materials={materials} />}
{tab === 'items' && <ItemsTab items={items} />}
{tab === 'craft' && <CraftTab recipes={recipes} materials={materials} />}
{tab === 'forge' && <ForgeTab />}
{tab === 'shop' && <ShopTab items={items} />}
</div>
);
}

View File

@@ -10,7 +10,6 @@ import { PlayerQuestArc } from '../quest/player-quest-arc.entity';
import { getUnlockedZones } from '../common/zone-access'; import { getUnlockedZones } from '../common/zone-access';
@Controller('monsters') @Controller('monsters')
@UseGuards(AuthGuard)
export class MonsterController { export class MonsterController {
constructor( constructor(
private readonly monsterService: MonsterService, private readonly monsterService: MonsterService,
@@ -22,7 +21,14 @@ export class MonsterController {
private readonly playerArcRepo: Repository<PlayerQuestArc>, private readonly playerArcRepo: Repository<PlayerQuestArc>,
) {} ) {}
@Get('bestiary')
async getBestiary() {
const allMonsters = await this.monsterService.findAll();
return allMonsters;
}
@Get('zones') @Get('zones')
@UseGuards(AuthGuard)
async getZones(@Req() req: Request) { async getZones(@Req() req: Request) {
const user = (req as any).user; const user = (req as any).user;
const char = await this.characterRepo.findOne({ where: { userId: user.id } }); const char = await this.characterRepo.findOne({ where: { userId: user.id } });
@@ -43,6 +49,7 @@ export class MonsterController {
} }
@Get() @Get()
@UseGuards(AuthGuard)
async findAll(@Req() req: Request) { async findAll(@Req() req: Request) {
const user = (req as any).user; const user = (req as any).user;
const char = await this.characterRepo.findOne({ where: { userId: user.id } }); const char = await this.characterRepo.findOne({ where: { userId: user.id } });