refactor: extract sub-components from monster functions (issue #99)
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m21s

- GuardianPactsTab: extracted GuardianCard, PactHeaderSummary, TierFilter + 5 helper components into guardian-pacts-components.tsx
- SpireSummaryTab: extracted TopStatsRow, NextGuardianCard, GuardianRoster, GuardianRosterItem, FloorLegend
- PrestigeTab: extracted InsightSummary, MemoriesCard, PactsCard, ResetLoopSection
- GameStateDebug: extracted WarningBanner, DisplayOptions, GameResetSection, ManaDebugSection, TimeControlSection, QuickActionsSection
- EquipmentCrafter: extracted CraftingProgress, BlueprintCard, BlueprintList, MaterialCard, MaterialsInventory
- PactDebug: extracted GuardianPactRow, GuardianPactList
- GameStateDebugSection: extracted DisplayOptions, GameResetSection, ManaDebugSection, TimeControlSection, QuickActionsSection
- PactDebugSection: extracted GuardianPactRow
- SpireCombatPage: extracted useSpireStats hook
- page.tsx: extracted GrimoireTab to separate file, useGameDerivedStats hook, TabTriggers, LazyTab wrapper

All files now under 400 lines. Build passes. All 639 tests pass.
This commit is contained in:
2026-05-20 18:38:24 +02:00
parent 53b3a94725
commit ce084a61a3
15 changed files with 1765 additions and 1539 deletions
+77
View File
@@ -0,0 +1,77 @@
'use client';
import { useState, useEffect } from 'react';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Badge } from '@/components/ui/badge';
import { DebugName } from '@/components/game/debug/debug-context';
import { SPELLS_DEF } from '@/lib/game/constants';
import type { SpellDef } from '@/lib/game/types';
export function GrimoireTab() {
const [grimoireSpells, setGrimoireSpells] = useState<[string, SpellDef][]>([]);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (typeof window !== 'undefined' && SPELLS_DEF) {
setGrimoireSpells(
Object.entries(SPELLS_DEF).filter((entry): entry is [string, SpellDef] => !!entry[1].grimoire)
);
}
setLoaded(true);
}, []);
if (!loaded) {
return <div className="p-4 text-center text-gray-400">Loading grimoire...</div>;
}
if (grimoireSpells.length === 0) {
return (
<div className="p-4 text-center text-gray-400">
No grimoire spells available yet. Defeat guardians to unlock spells.
</div>
);
}
const availablePages = Math.ceil(grimoireSpells.length / 12);
return (
<DebugName name="GrimoireTab">
<div className="space-y-4">
<div className="text-sm text-gray-400">
<p className="mb-2">A vast tome of arcane knowledge. Study carefully each spell costs insight to transcribe into your repertoire.</p>
<p>Available pages: {availablePages}. Spells in grimoire: {grimoireSpells.length}.</p>
</div>
<ScrollArea className="h-[600px] rounded border border-gray-700 p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{grimoireSpells.map(([id, spell]) => (
<div
key={id}
className="p-4 bg-gray-800/50 rounded border border-gray-600 hover:border-gray-500 transition-colors"
>
<div className="flex items-start justify-between mb-2">
<span className="font-bold text-gray-100">{spell.name}</span>
<Badge variant="outline" className="border-gray-600">
{spell.elem}
</Badge>
</div>
{spell.desc && <p className="text-sm text-gray-400 mb-3">{spell.desc}</p>}
<div className="text-xs text-gray-500 space-y-1">
<div>Cost: {spell.cost.amount} {
spell.cost.type === 'element'
? spell.cost.element
: 'raw mana'
}</div>
<div>Power: {spell.dmg}</div>
{spell.effects && spell.effects.length > 0 && (
<div>Effects: {spell.effects.map(e => e.type).join(', ')}</div>
)}
</div>
</div>
))}
</div>
</ScrollArea>
</div>
</DebugName>
);
}
+104 -240
View File
@@ -3,7 +3,6 @@
import { useEffect, useState, lazy, Suspense } from 'react';
import { useShallow } from 'zustand/react/shallow';
// Import from new modular stores
import {
useGameStore,
useUIStore,
@@ -20,148 +19,68 @@ import {
} from '@/lib/game/stores';
import { computeDisciplineEffects } from '@/lib/game/effects/discipline-effects';
import { useGameLoop } from '@/lib/game/stores/gameHooks';
import { getUnifiedEffects, type UnifiedEffects } from '@/lib/game/effects';
import { SPELLS_DEF } from '@/lib/game/constants';
import { TimeDisplay } from '@/components/game';
import { getUnifiedEffects } from '@/lib/game/effects';
import { hasSpecial, SPECIAL_EFFECTS } from '@/lib/game/effects';
import type { SpellDef } from '@/lib/game/types';
import { TimeDisplay } from '@/components/game';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { TooltipProvider } from '@/components/ui/tooltip';
import { ErrorBoundary } from '@/components/ErrorBoundary';
import { DebugName } from '@/components/game/debug/debug-context';
// Import extracted components
import { GameOverScreen } from './components/GameOverScreen';
import { LeftPanel } from './components/LeftPanel';
import { GrimoireTab } from './components/GrimoireTab';
// Lazy load tab components
const DisciplinesTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DisciplinesTab })));
const SpellsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpellsTab })));
const StatsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.StatsTab })));
const DebugTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.DebugTab })));
const AchievementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AchievementsTab })));
const AttunementsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.AttunementsTab })));
const PrestigeTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.PrestigeTab })));
const EquipmentTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.EquipmentTab })));
const GolemancyTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GolemancyTab })));
const GuardianPactsTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.GuardianPactsTab })));
const SpireSummaryTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.SpireSummaryTab })));
const CraftingTab = lazy(() => import('@/components/game/tabs').then(module => ({ default: module.CraftingTab })));
const SpireCombatPage = lazy(() => import('@/components/game/tabs/SpireCombatPage').then(module => ({ default: module.SpireCombatPage })));
const DisciplinesTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.DisciplinesTab })));
const SpellsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.SpellsTab })));
const StatsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.StatsTab })));
const DebugTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.DebugTab })));
const AchievementsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.AchievementsTab })));
const AttunementsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.AttunementsTab })));
const PrestigeTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.PrestigeTab })));
const EquipmentTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.EquipmentTab })));
const GolemancyTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.GolemancyTab })));
const GuardianPactsTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.GuardianPactsTab })));
const SpireSummaryTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.SpireSummaryTab })));
const CraftingTab = lazy(() => import('@/components/game/tabs').then(m => ({ default: m.CraftingTab })));
const SpireCombatPage = lazy(() => import('@/components/game/tabs/SpireCombatPage').then(m => ({ default: m.SpireCombatPage })));
const TabLoadingFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>;
const TabFallback = () => <div className="p-4 text-center text-gray-400">Loading...</div>;
// ============================================================================
// Grimoire Tab Component
// ============================================================================
function GrimoireTab() {
const [grimoireSpells, setGrimoireSpells] = useState<[string, SpellDef][]>(() => {
if (typeof window !== 'undefined' && SPELLS_DEF) {
return Object.entries(SPELLS_DEF).filter((entry): entry is [string, SpellDef] => !!entry[1].grimoire);
}
return [];
});
const loaded = typeof window !== 'undefined';
if (!loaded) {
return <div className="p-4 text-center text-gray-400">Loading grimoire...</div>;
}
if (grimoireSpells.length === 0) {
return (
<div className="p-4 text-center text-gray-400">
No grimoire spells available yet. Defeat guardians to unlock spells.
</div>
);
}
const availablePages = Math.ceil(grimoireSpells.length / 12);
return (
<DebugName name="GrimoireTab">
<div className="space-y-4">
<div className="text-sm text-gray-400">
<p className="mb-2">A vast tome of arcane knowledge. Study carefully each spell costs insight to transcribe into your repertoire.</p>
<p>Available pages: {availablePages}. Spells in grimoire: {grimoireSpells.length}.</p>
</div>
<ScrollArea className="h-[600px] rounded border border-gray-700 p-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{grimoireSpells.map(([id, spell]) => (
<div
key={id}
className="p-4 bg-gray-800/50 rounded border border-gray-600 hover:border-gray-500 transition-colors"
>
<div className="flex items-start justify-between mb-2">
<span className="font-bold text-gray-100">{spell.name}</span>
<Badge variant="outline" className="border-gray-600">
{spell.elem}
</Badge>
</div>
{spell.desc && <p className="text-sm text-gray-400 mb-3">{spell.desc}</p>}
<div className="text-xs text-gray-500 space-y-1">
<div>Cost: {spell.cost.amount} {
spell.cost.type === 'element'
? spell.cost.element
: 'raw mana'
}</div>
<div>Power: {spell.dmg}</div>
{spell.effects && spell.effects.length > 0 && <div>Effects: {spell.effects.map(e => e.type).join(', ')}</div>}
</div>
</div>
))}
</div>
</ScrollArea>
</div>
</DebugName>
);
function TabErrorFallback({ name }: { name: string }) {
return <div className="p-4 text-red-400">{name} tab failed to load.</div>;
}
// ============================================================================
// Main Game Component
// ============================================================================
// ─── Derived Stats Hook ──────────────────────────────────────────────────────
export default function ManaLoopGame() {
const [selectedManaType, setSelectedManaType] = useState<string>('');
const [activeTab, setActiveTab] = useState('spells');
// ALL hooks must be called before any conditional returns
useGameLoop();
// Use useShallow to combine multi-field subscriptions and reduce re-renders
const { day, hour, initGame } = useGameStore(useShallow(s => ({ day: s.day, hour: s.hour, initGame: s.initGame })));
const { prestigeUpgrades, insight, loopInsight } = usePrestigeStore(useShallow(s => ({ prestigeUpgrades: s.prestigeUpgrades, insight: s.insight, loopInsight: s.loopInsight })));
const { rawMana, meditateTicks } = useManaStore(useShallow(s => ({ rawMana: s.rawMana, meditateTicks: s.meditateTicks })));
const spireMode = useCombatStore((s) => s.spireMode);
const gameOver = useUIStore((s) => s.gameOver);
// Get equipment state from crafting store
function useGameDerivedStats() {
const { prestigeUpgrades } = usePrestigeStore(useShallow(s => ({
prestigeUpgrades: s.prestigeUpgrades,
})));
const { meditateTicks } = useManaStore(useShallow(s => ({
meditateTicks: s.meditateTicks,
})));
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
const day = useGameStore((s) => s.day);
const hour = useGameStore((s) => s.hour);
// Derived state
const upgradeEffects = getUnifiedEffects({
skillUpgrades: {},
skillTiers: {},
equippedInstances,
equipmentInstances
equipmentInstances,
});
// Compute discipline bonuses from active disciplines
const disciplineEffects = computeDisciplineEffects();
const maxMana = computeMaxMana({
skills: {},
prestigeUpgrades,
skillUpgrades: {},
skillTiers: {}
skillTiers: {},
}, upgradeEffects, disciplineEffects);
const baseRegen = computeRegen({
@@ -172,30 +91,80 @@ export default function ManaLoopGame() {
attunements: {},
}, upgradeEffects, disciplineEffects);
const clickMana = computeClickMana({
skills: {},
}, disciplineEffects);
const clickMana = computeClickMana({ skills: {} }, disciplineEffects);
const meditationMultiplier = getMeditationBonus(meditateTicks, {}, upgradeEffects.meditationEfficiency);
const incursionStrength = getIncursionStrength(day, hour);
// Effective regen with incursion penalty
const effectiveRegenWithSpecials = baseRegen * (1 - incursionStrength);
// Mana Cascade bonus
const manaCascadeBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_CASCADE)
? Math.floor(maxMana / 100) * 0.1
: 0;
// Mana Waterfall bonus
const manaWaterfallBonus = hasSpecial(upgradeEffects, SPECIAL_EFFECTS.MANA_WATERFALL)
? Math.floor(maxMana / 100) * 0.25
: 0;
// Effective regen
const effectiveRegen = (effectiveRegenWithSpecials + manaCascadeBonus + manaWaterfallBonus) * meditationMultiplier;
// Initialize game on mount
return { maxMana, effectiveRegen, clickMana, meditationMultiplier };
}
// ─── Tab Triggers ────────────────────────────────────────────────────────────
function TabTriggers() {
return (
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
<TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
<TabsTrigger value="disciplines" className="text-xs px-2 py-1">📚 Disciplines</TabsTrigger>
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
<TabsTrigger value="debug" className="text-xs px-2 py-1">🐛 Debug</TabsTrigger>
<TabsTrigger value="attunements" className="text-xs px-2 py-1"> Attunements</TabsTrigger>
<TabsTrigger value="achievements" className="text-xs px-2 py-1">🏆 Achievements</TabsTrigger>
<TabsTrigger value="prestige" className="text-xs px-2 py-1"> Prestige</TabsTrigger>
<TabsTrigger value="equipment" className="text-xs px-2 py-1"> Equipment</TabsTrigger>
<TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golemancy</TabsTrigger>
<TabsTrigger value="pacts" className="text-xs px-2 py-1">📜 Pacts</TabsTrigger>
<TabsTrigger value="spire" className="text-xs px-2 py-1">🏔 Spire</TabsTrigger>
<TabsTrigger value="crafting" className="text-xs px-2 py-1"> Crafting</TabsTrigger>
</TabsList>
);
}
// ─── Lazy Tab Content ────────────────────────────────────────────────────────
function LazyTab({ name, children }: { name: string; children: React.ReactNode }) {
return (
<ErrorBoundary fallback={<TabErrorFallback name={name} />}>
<Suspense fallback={<TabFallback />}>
{children}
</Suspense>
</ErrorBoundary>
);
}
// ─── Main Game Component ─────────────────────────────────────────────────────
export default function ManaLoopGame() {
const [activeTab, setActiveTab] = useState('spells');
useGameLoop();
const { day, hour, initGame } = useGameStore(useShallow(s => ({
day: s.day,
hour: s.hour,
initGame: s.initGame,
})));
const { insight, loopInsight } = usePrestigeStore(useShallow(s => ({
insight: s.insight,
loopInsight: s.loopInsight,
})));
const spireMode = useCombatStore((s) => s.spireMode);
const gameOver = useUIStore((s) => s.gameOver);
useGameDerivedStats();
useEffect(() => {
initGame();
}, [initGame]);
@@ -203,21 +172,18 @@ export default function ManaLoopGame() {
const [mounted, setMounted] = useState(false);
useEffect(() => { setMounted(true); }, []); // eslint-disable-line react-hooks/set-state-in-effect
// React to spireMode changes from combat store
useEffect(() => {
if (spireMode) {
setActiveTab('spells'); // eslint-disable-line react-hooks/set-state-in-effect
}
}, [spireMode]);
// Conditional returns AFTER all hooks
if (gameOver) {
return <GameOverScreen day={day} hour={hour} insightGained={loopInsight} totalInsight={insight} />;
}
if (!mounted) return <div className="p-4 text-center text-gray-400">Loading...</div>;
// Spire mode: full-page replacement view
if (spireMode) {
return (
<ErrorBoundary>
@@ -232,7 +198,6 @@ export default function ManaLoopGame() {
<ErrorBoundary>
<TooltipProvider>
<div className="game-root min-h-screen flex flex-col">
{/* Header */}
<header className="sticky top-0 z-50 bg-gradient-to-b from-gray-900 to-gray-900/80 border-b border-gray-700 px-4 py-2">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold game-title tracking-wider">MANA LOOP</h1>
@@ -242,127 +207,26 @@ export default function ManaLoopGame() {
</div>
</header>
{/* Main Content */}
<main className="flex-1 flex flex-col md:flex-row gap-4 p-4">
<LeftPanel />
<div className="flex-1 min-w-0">
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="flex flex-wrap gap-1 w-full mb-4 h-auto">
<TabsTrigger value="spells" className="text-xs px-2 py-1">🔮 Spells</TabsTrigger>
<TabsTrigger value="stats" className="text-xs px-2 py-1">📊 Stats</TabsTrigger>
<TabsTrigger value="disciplines" className="text-xs px-2 py-1">📚 Disciplines</TabsTrigger>
<TabsTrigger value="grimoire" className="text-xs px-2 py-1">📖 Grimoire</TabsTrigger>
<TabsTrigger value="debug" className="text-xs px-2 py-1">🐛 Debug</TabsTrigger>
<TabsTrigger value="attunements" className="text-xs px-2 py-1"> Attunements</TabsTrigger>
<TabsTrigger value="achievements" className="text-xs px-2 py-1">🏆 Achievements</TabsTrigger>
<TabsTrigger value="prestige" className="text-xs px-2 py-1"> Prestige</TabsTrigger>
<TabsTrigger value="equipment" className="text-xs px-2 py-1"> Equipment</TabsTrigger>
<TabsTrigger value="golemancy" className="text-xs px-2 py-1">🗿 Golemancy</TabsTrigger>
<TabsTrigger value="pacts" className="text-xs px-2 py-1">📜 Pacts</TabsTrigger>
<TabsTrigger value="spire" className="text-xs px-2 py-1">🏔 Spire</TabsTrigger>
<TabsTrigger value="crafting" className="text-xs px-2 py-1"> Crafting</TabsTrigger>
</TabsList>
<TabTriggers />
<TabsContent value="spells">
<ErrorBoundary fallback={<div className="p-4 text-red-400">spells tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<SpellsTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="stats">
<ErrorBoundary fallback={<div className="p-4 text-red-400">stats tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<StatsTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="disciplines">
<ErrorBoundary fallback={<div className="p-4 text-red-400">disciplines tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<DisciplinesTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="grimoire">
<GrimoireTab />
</TabsContent>
<TabsContent value="debug">
<ErrorBoundary fallback={<div className="p-4 text-red-400">debug tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<DebugTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="attunements">
<ErrorBoundary fallback={<div className="p-4 text-red-400">attunements tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<AttunementsTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="achievements">
<ErrorBoundary fallback={<div className="p-4 text-red-400">achievements tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<AchievementsTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="prestige">
<ErrorBoundary fallback={<div className="p-4 text-red-400">prestige tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<PrestigeTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="equipment">
<ErrorBoundary fallback={<div className="p-4 text-red-400">equipment tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<EquipmentTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="golemancy">
<ErrorBoundary fallback={<div className="p-4 text-red-400">golemancy tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<GolemancyTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="pacts">
<ErrorBoundary fallback={<div className="p-4 text-red-400">pacts tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<GuardianPactsTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="spire">
<ErrorBoundary fallback={<div className="p-4 text-red-400">spire tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<SpireSummaryTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="crafting">
<ErrorBoundary fallback={<div className="p-4 text-red-400">crafting tab failed to load.</div>}>
<Suspense fallback={<TabLoadingFallback />}>
<CraftingTab />
</Suspense>
</ErrorBoundary>
</TabsContent>
<TabsContent value="spells"><LazyTab name="spells"><SpellsTab /></LazyTab></TabsContent>
<TabsContent value="stats"><LazyTab name="stats"><StatsTab /></LazyTab></TabsContent>
<TabsContent value="disciplines"><LazyTab name="disciplines"><DisciplinesTab /></LazyTab></TabsContent>
<TabsContent value="grimoire"><GrimoireTab /></TabsContent>
<TabsContent value="debug"><LazyTab name="debug"><DebugTab /></LazyTab></TabsContent>
<TabsContent value="attunements"><LazyTab name="attunements"><AttunementsTab /></LazyTab></TabsContent>
<TabsContent value="achievements"><LazyTab name="achievements"><AchievementsTab /></LazyTab></TabsContent>
<TabsContent value="prestige"><LazyTab name="prestige"><PrestigeTab /></LazyTab></TabsContent>
<TabsContent value="equipment"><LazyTab name="equipment"><EquipmentTab /></LazyTab></TabsContent>
<TabsContent value="golemancy"><LazyTab name="golemancy"><GolemancyTab /></LazyTab></TabsContent>
<TabsContent value="pacts"><LazyTab name="pacts"><GuardianPactsTab /></LazyTab></TabsContent>
<TabsContent value="spire"><LazyTab name="spire"><SpireSummaryTab /></LazyTab></TabsContent>
<TabsContent value="crafting"><LazyTab name="crafting"><CraftingTab /></LazyTab></TabsContent>
</Tabs>
</div>
</main>