refactor: cleanup codebase — remove hydration guards, extract constants, fix bugs
Build and Publish Mana Loop Docker Image / build-and-publish (push) Successful in 1m20s

This commit is contained in:
2026-05-26 11:20:36 +02:00
parent 5c64bb00fa
commit b402b8f56e
23 changed files with 579 additions and 979 deletions
+1 -15
View File
@@ -1,6 +1,6 @@
'use client';
import { useState, useMemo, useEffect } from 'react';
import { useState, useMemo } from 'react';
import { useCombatStore } from '@/lib/game/stores';
import {
ACHIEVEMENTS,
@@ -164,14 +164,8 @@ function CategorySection({
export function AchievementsTab() {
const achievements = useCombatStore((s) => s.achievements);
const [mounted, setMounted] = useState(false);
const [collapsedCategories, setCollapsedCategories] = useState<Record<string, boolean>>({});
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true);
}, []);
const byCategory = useMemo(() => getAchievementsByCategory(), []);
const categories = useMemo(
() => Object.keys(byCategory).sort(),
@@ -188,14 +182,6 @@ export function AchievementsTab() {
}));
};
if (!mounted) {
return (
<div className="flex items-center justify-center p-8 text-gray-500">
Loading achievements
</div>
);
}
return (
<DebugName name="AchievementsTab">
<div className="space-y-4">
+1 -15
View File
@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect } from 'react';
import { useState } from 'react';
import { useAttunementStore } from '@/lib/game/stores';
import { ATTUNEMENTS_DEF, ATTUNEMENT_SLOT_NAMES, getAttunementXPForLevel, MAX_ATTUNEMENT_LEVEL } from '@/lib/game/data/attunements';
import type { AttunementDef, AttunementState } from '@/lib/game/types';
@@ -157,24 +157,10 @@ function AttunementCard({ def, state }: AttunementCardProps) {
export function AttunementsTab() {
const attunements = useAttunementStore((s) => s.attunements);
const [mounted, setMounted] = useState(false);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true);
}, []);
const allDefs = Object.values(ATTUNEMENTS_DEF);
const unlockedCount = allDefs.filter((d) => isAttunementUnlocked(d.id, attunements)).length;
if (!mounted) {
return (
<div className="flex items-center justify-center p-8 text-gray-500">
Loading attunements
</div>
);
}
return (
<DebugName name="AttunementsTab">
<div className="space-y-4">
+1 -15
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useState, useCallback } from 'react';
import React, { useState, useCallback } from 'react';
import { useDisciplineStore } from '@/lib/game/stores/discipline-slice';
import type { DisciplineDefinition } from '@/lib/game/types/disciplines';
import type { ManaType } from '@/lib/game/types/elements';
@@ -201,14 +201,8 @@ export const DisciplinesTab: React.FC = () => {
const activate = useDisciplineStore((s) => s.activate);
const deactivate = useDisciplineStore((s) => s.deactivate);
const [mounted, setMounted] = useState(false);
const [activeAttunement, setActiveAttunement] = useState<string>('base');
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true);
}, []);
const handleToggle = useCallback((id: string, paused: boolean) => {
if (paused) {
activate(id);
@@ -217,14 +211,6 @@ export const DisciplinesTab: React.FC = () => {
}
}, [activate, deactivate]);
if (!mounted) {
return (
<div className="flex items-center justify-center p-8 text-gray-500">
Loading disciplines
</div>
);
}
const activeTab = ATTUNEMENT_TABS.find((t) => t.key === activeAttunement);
return (
+1 -16
View File
@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect, useCallback, useMemo } from 'react';
import { useState, useCallback, useMemo } from 'react';
import { useCraftingStore } from '@/lib/game/stores/craftingStore';
import type { EquipmentSlot } from '@/lib/game/types';
import { DebugName } from '@/components/game/debug/debug-context';
@@ -9,19 +9,12 @@ import { InventoryList } from './EquipmentTab/InventoryList';
import { EquipmentEffectsSummary } from './EquipmentTab/EquipmentEffectsSummary';
export function EquipmentTab() {
const [mounted, setMounted] = useState(false);
const equippedInstances = useCraftingStore((s) => s.equippedInstances);
const equipmentInstances = useCraftingStore((s) => s.equipmentInstances);
const storeEquipItem = useCraftingStore((s) => s.equipItem);
const storeUnequipItem = useCraftingStore((s) => s.unequipItem);
const storeDeleteEquipment = useCraftingStore((s) => s.deleteEquipmentInstance);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true);
}, []);
const handleEquip = useCallback(
(instanceId: string, slot: EquipmentSlot): boolean => {
return storeEquipItem(instanceId, slot);
@@ -51,14 +44,6 @@ export function EquipmentTab() {
[equipmentInstances, equippedInstances]
);
if (!mounted) {
return (
<div className="flex items-center justify-center p-8 text-[var(--text-muted)]">
Loading equipment
</div>
);
}
return (
<DebugName name="EquipmentTab">
<div className="space-y-6">
+1 -15
View File
@@ -1,6 +1,6 @@
'use client';
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useCombatStore } from '@/lib/game/stores/combatStore';
import { useAttunementStore } from '@/lib/game/stores/attunementStore';
@@ -197,7 +197,6 @@ GolemCard.displayName = 'GolemCard';
// ─── Main Tab ────────────────────────────────────────────────────────────────
export const GolemancyTab: React.FC = () => {
const [mounted, setMounted] = useState(false);
const [activeTier, setActiveTier] = useState<string>('base');
const { golemancy, toggleGolem } = useCombatStore(useShallow(s => ({
@@ -210,11 +209,6 @@ export const GolemancyTab: React.FC = () => {
elements: s.elements,
})));
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true);
}, []);
// Build attunement lookup for isGolemUnlocked
const attunementLookup = useMemo(() => {
const lookup: Record<string, { active: boolean; level: number }> = {};
@@ -254,14 +248,6 @@ export const GolemancyTab: React.FC = () => {
const golemSlots = getGolemSlots(fabricatorLevel);
const enabledCount = golemancy.enabledGolems.length;
if (!mounted) {
return (
<div className="flex items-center justify-center p-8 text-gray-500">
Loading golemancy
</div>
);
}
const activeTierGolems = golemsByTier[activeTier] ?? [];
return (
+1 -15
View File
@@ -1,6 +1,6 @@
'use client';
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import React, { useState, useMemo, useCallback } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { usePrestigeStore } from '@/lib/game/stores/prestigeStore';
import { useManaStore } from '@/lib/game/stores/manaStore';
@@ -53,7 +53,6 @@ function groupFloorsByTier(floors: number[]): FloorTier[] {
// ─── Main Tab ────────────────────────────────────────────────────────────────
export const GuardianPactsTab: React.FC = () => {
const [mounted, setMounted] = useState(false);
const [activeTier, setActiveTier] = useState<string>('all');
const {
@@ -75,11 +74,6 @@ export const GuardianPactsTab: React.FC = () => {
const rawMana = useManaStore(s => s.rawMana);
const addLog = useUIStore(s => s.addLog);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true);
}, []);
const guardianFloors = useMemo(
() => getAllGuardianFloors(),
[],
@@ -126,14 +120,6 @@ export const GuardianPactsTab: React.FC = () => {
return boonMap;
}, [signedPacts]);
if (!mounted) {
return (
<div className="flex items-center justify-center p-8 text-gray-500">
Loading guardian pacts
</div>
);
}
return (
<DebugName name="GuardianPactsTab">
<div className="space-y-4">
+1 -16
View File
@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { useState, useCallback } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { usePrestigeStore, useGameStore } from '@/lib/game/stores';
import { PRESTIGE_DEF } from '@/lib/game/constants/prestige';
@@ -186,8 +186,6 @@ function ResetLoopSection({ loopInsight, onReset }: { loopInsight: number; onRes
// ─── Main Component ───────────────────────────────────────────────────────────
export function PrestigeTab() {
const [mounted, setMounted] = useState(false);
const {
insight,
totalInsight,
@@ -212,11 +210,6 @@ export function PrestigeTab() {
const startNewLoop = useGameStore((s) => s.startNewLoop);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true);
}, []);
const handlePurchase = useCallback((id: string) => {
doPrestige(id);
}, [doPrestige]);
@@ -225,14 +218,6 @@ export function PrestigeTab() {
startNewLoop();
}, [startNewLoop]);
if (!mounted) {
return (
<div className="flex items-center justify-center p-8 text-gray-500">
Loading prestige
</div>
);
}
const upgradeEntries = Object.entries(PRESTIGE_DEF);
return (
@@ -71,7 +71,7 @@ function EnemyRow({ enemy }: { enemy: EnemyState }) {
);
}
export function RoomDisplay({ floorState, _floor }: RoomDisplayProps) {
export function RoomDisplay({ floorState }: RoomDisplayProps) {
// Guard against null/undefined/stale floorState
if (!floorState || !floorState.roomType) {
return (
@@ -47,7 +47,6 @@ function useSpireStats(prestigeUpgrades: Record<string, number>, equippedInstanc
// ─── Main Component ───────────────────────────────────────────────────────────
export function SpireCombatPage() {
const [mounted, setMounted] = useState(false);
const [roomsCleared, setRoomsCleared] = useState(0);
const {
@@ -104,8 +103,6 @@ export function SpireCombatPage() {
const totalRooms = useMemo(() => getRoomsForFloor(currentFloor), [currentFloor]);
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true);
setRoomsCleared(0);
const newRoom = generateSpireFloorState(currentFloor, 0, totalRooms);
setCurrentRoom(newRoom);
@@ -166,14 +163,6 @@ export function SpireCombatPage() {
addActivityLog('floor_transition', '🚪 Exited the Spire.');
};
if (!mounted) {
return (
<div className="flex items-center justify-center min-h-screen text-gray-500">
Loading spire...
</div>
);
}
return (
<div className="min-h-screen bg-gray-950 flex flex-col">
<header className="sticky top-0 z-50 bg-gray-900/95 border-b border-gray-800 px-4 py-2">
+1 -16
View File
@@ -1,6 +1,6 @@
'use client';
import { useState, useEffect, useMemo } from 'react';
import { useState, useMemo } from 'react';
import { useShallow } from 'zustand/react/shallow';
import { useCombatStore, usePrestigeStore, fmt } from '@/lib/game/stores';
import { ELEMENT_OPPOSITES, FLOOR_ELEM_CYCLE } from '@/lib/game/constants';
@@ -311,8 +311,6 @@ function GuardianRosterItem({ floor, guardian, isDefeated }: { floor: number; gu
// ─── Main Component ───────────────────────────────────────────────────────────
export function SpireSummaryTab() {
const [mounted, setMounted] = useState(false);
const {
maxFloorReached,
clearedFloors,
@@ -327,11 +325,6 @@ export function SpireSummaryTab() {
insight: s.insight,
})));
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
setMounted(true);
}, []);
const defeatedGuardians = useMemo(() => {
return GUARDIAN_FLOORS.filter((floor) => clearedFloors[floor]);
}, [clearedFloors]);
@@ -346,14 +339,6 @@ export function SpireSummaryTab() {
return Object.values(clearedFloors).filter(Boolean).length;
}, [clearedFloors]);
if (!mounted) {
return (
<div className="flex items-center justify-center p-8 text-gray-500">
Loading spire data
</div>
);
}
return (
<DebugName name="SpireSummaryTab">
<div className="space-y-4">