From 86683fe2885003563d31bfa8c13f0c9559b3d5cd Mon Sep 17 00:00:00 2001 From: Refactoring Agent <[email protected]> Date: Fri, 1 May 2026 15:46:11 +0200 Subject: [PATCH] refactor: extract components from EquipmentTab.tsx to reduce below 400 lines --- .../game/tabs/EnchantmentsPanel.tsx | 48 ++ .../game/tabs/EquipmentControls.tsx | 76 +++ .../game/tabs/EquipmentInventory.tsx | 187 ++++++++ .../game/tabs/EquipmentSlotGrid.tsx | 235 +++++++++ src/components/game/tabs/EquipmentTab.tsx | 450 +++++------------- 5 files changed, 662 insertions(+), 334 deletions(-) create mode 100644 src/components/game/tabs/EnchantmentsPanel.tsx create mode 100644 src/components/game/tabs/EquipmentControls.tsx create mode 100644 src/components/game/tabs/EquipmentInventory.tsx create mode 100644 src/components/game/tabs/EquipmentSlotGrid.tsx diff --git a/src/components/game/tabs/EnchantmentsPanel.tsx b/src/components/game/tabs/EnchantmentsPanel.tsx new file mode 100644 index 0000000..b20e234 --- /dev/null +++ b/src/components/game/tabs/EnchantmentsPanel.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { Badge } from '@/components/ui/badge'; + +interface EnchantmentsPanelProps { + enchantments: Array<{ effectId: string; stacks: number }>; + compact?: boolean; +} + +export function EnchantmentsPanel({ + enchantments, + compact = false, +}: EnchantmentsPanelProps) { + if (enchantments.length === 0) { + return null; + } + + return ( +
+ {enchantments.map((ench, i) => { + const effect = ENCHANTMENT_EFFECTS[ench.effectId]; + return ( + + + + + {effect?.name || ench.effectId} + {ench.stacks > 1 && ` x${ench.stacks}`} + + + +

{effect?.description || 'Unknown effect'}

+

+ Category: {effect?.category || 'unknown'} +

+
+
+
+ ); + })} +
+ ); +} diff --git a/src/components/game/tabs/EquipmentControls.tsx b/src/components/game/tabs/EquipmentControls.tsx new file mode 100644 index 0000000..09460c9 --- /dev/null +++ b/src/components/game/tabs/EquipmentControls.tsx @@ -0,0 +1,76 @@ +'use client'; + +import { EquipmentSlot } from '@/lib/game/data/equipment'; +import type { GameStore } from '@/lib/game/types'; +import { ActionButton } from '@/components/ui/action-button'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { X } from 'lucide-react'; + +interface EquipmentControlsProps { + store: GameStore; + onUnequip: (slot: EquipmentSlot) => void; + onDelete: (instanceId: string, name: string) => void; + selectedSlot: EquipmentSlot | null; + isSlotBlocked: (slot: EquipmentSlot) => boolean; + isEquipped: (instanceId: string) => boolean; + getEquippableSlots: (typeId: string) => EquipmentSlot[]; +} + +export function EquipmentControls({ + store, + onUnequip, + onDelete, + selectedSlot, + isSlotBlocked, + isEquipped, + getEquippableSlots, +}: EquipmentControlsProps) { + const SLOT_NAMES = { + mainHand: 'Main Hand', + offHand: 'Off Hand', + head: 'Head', + body: 'Body', + hands: 'Hands', + feet: 'Feet', + accessory1: 'Accessory 1', + accessory2: 'Accessory 2', + } as const; + + return { + renderUnequipButton: (slot: EquipmentSlot, instanceName: string) => ( + { + e.stopPropagation(); + onUnequip(slot); + }} + aria-label={`Unequip ${instanceName}`} + > + + + ), + + renderDeleteButton: (instanceId: string, name: string) => ( + + + + onDelete(instanceId, name)} + aria-label={`Delete ${name}`} + > + + + + +

Delete this item

+
+
+
+ ), + }; +} diff --git a/src/components/game/tabs/EquipmentInventory.tsx b/src/components/game/tabs/EquipmentInventory.tsx new file mode 100644 index 0000000..49cddde --- /dev/null +++ b/src/components/game/tabs/EquipmentInventory.tsx @@ -0,0 +1,187 @@ +'use client'; + +import { useState } from 'react'; +import { EquipmentSlot } from '@/lib/game/data/equipment'; +import { EQUIPMENT_TYPES } from '@/lib/game/data/equipment'; +import type { GameStore, EquipmentInstance } from '@/lib/game/types'; +import { GameCard } from '@/components/ui/game-card'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { EnchantmentsPanel } from './EnchantmentsPanel'; + +interface EquipmentInventoryProps { + store: GameStore; + unequippedItems: EquipmentInstance[]; + onEquip: (instanceId: string, slot: EquipmentSlot) => void; + onDelete: (instanceId: string, name: string) => void; + getEquippableSlots: (typeId: string) => EquipmentSlot[]; + SLOT_NAMES: Record; + SLOT_ICONS: Record; +} + +export function EquipmentInventory({ + store, + unequippedItems, + onEquip, + onDelete, + getEquippableSlots, + SLOT_NAMES, + SLOT_ICONS, +}: EquipmentInventoryProps) { + return ( +
+ {unequippedItems.map((instance) => { + const equipmentType = EQUIPMENT_TYPES[instance.typeId]; + const validSlots = getEquippableSlots(instance.typeId); + + return ( + +
+
+
+ {instance.name} +
+
+ {equipmentType?.description} +
+
+ + {equipmentType?.category || 'unknown'} + +
+ +
+
+ Capacity: {instance.usedCapacity}/{instance.totalCapacity} + {instance.quality < 100 && ( + + (Quality: {instance.quality}%) + + )} +
+ {instance.enchantments.length > 0 && ( + + )} +
+ + {validSlots.length > 0 && ( + + )} +
+ ); + })} +
+ ); +} + +function getRarityBorderColor(rarity: string) { + const colors: Record = { + common: 'border-[var(--text-muted)]', + uncommon: 'border-[var(--color-success)]', + rare: 'border-[var(--mana-water)]', + epic: 'border-[var(--mana-stellar)]', + legendary: 'border-[var(--mana-light)]', + mythic: 'border-[var(--mana-dark)]', + }; + return colors[rarity] || 'border-[var(--border-default)]'; +} + +function getRarityBgColor(rarity: string) { + const colors: Record = { + common: 'bg-[var(--bg-sunken)]/30', + uncommon: 'bg-[var(--color-success)]/10', + rare: 'bg-[var(--mana-water)]/10', + epic: 'bg-[var(--mana-stellar)]/10', + legendary: 'bg-[var(--mana-light)]/10', + mythic: 'bg-[var(--mana-dark)]/10', + }; + return colors[rarity] || ''; +} + +function getRarityTextColor(rarity: string) { + const colors: Record = { + common: 'text-[var(--text-secondary)]', + uncommon: 'text-[var(--color-success)]', + rare: 'text-[var(--mana-water)]', + epic: 'text-[var(--mana-stellar)]', + legendary: 'text-[var(--mana-light)]', + mythic: 'text-[var(--mana-dark)]', + }; + return colors[rarity] || 'text-[var(--text-primary)]'; +} + +interface EquipControlsProps { + instance: EquipmentInstance; + validSlots: EquipmentSlot[]; + onEquip: (instanceId: string, slot: EquipmentSlot) => void; + onDelete: (instanceId: string, name: string) => void; + SLOT_NAMES: Record; + SLOT_ICONS: Record; +} + +function EquipControls({ + instance, + validSlots, + onEquip, + onDelete, + SLOT_NAMES, + SLOT_ICONS, +}: EquipControlsProps) { + return ( +
+ + + +
+ ); +} diff --git a/src/components/game/tabs/EquipmentSlotGrid.tsx b/src/components/game/tabs/EquipmentSlotGrid.tsx new file mode 100644 index 0000000..957e6cf --- /dev/null +++ b/src/components/game/tabs/EquipmentSlotGrid.tsx @@ -0,0 +1,235 @@ +'use client'; + +import { EquipmentSlot } from '@/lib/game/data/equipment'; +import { SLOT_NAMES } from './EquipmentTab'; +import type { GameStore, EquipmentInstance } from '@/lib/game/types'; +import { GameCard } from '@/components/ui/game-card'; +import { Badge } from '@/components/ui/badge'; +import { AlertCircle, Sword, Shield, HardHat, Shirt, Hand, Footprints, Gem } from 'lucide-react'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'; +import { RARITY_BORDER_COLORS, RARITY_BG_COLORS, RARITY_TEXT_COLORS } from './EquipmentTab'; +import { ENCHANTMENT_EFFECTS } from '@/lib/game/data/enchantment-effects'; +import type { EquipmentType } from '@/lib/game/data/equipment'; + +const SLOT_ICONS: Record = { + mainHand: Sword, + offHand: Shield, + head: HardHat, + body: Shirt, + hands: Hand, + feet: Footprints, + accessory1: Gem, + accessory2: Gem, +}; + +interface EquipmentSlotGridProps { + store: GameStore; + selectedSlot: EquipmentSlot | null; + onSlotClick: (slot: EquipmentSlot) => void; + onUnequip: (slot: EquipmentSlot) => void; + isSlotBlocked: (slot: EquipmentSlot) => boolean; + SLOT_GROUPS: Array<{ label: string; slots: EquipmentSlot[] }>; +} + +export function EquipmentSlotGrid({ + store, + selectedSlot, + onSlotClick, + onUnequip, + isSlotBlocked, + SLOT_GROUPS, +}: EquipmentSlotGridProps) { + const renderSlot = (slot: EquipmentSlot) => { + const instanceId = store.equippedInstances[slot]; + const instance = instanceId ? store.equipmentInstances[instanceId] : null; + const equipmentType = instance ? (store as any).EQUIPMENT_TYPES?.[instance.typeId] : null; + const blocked = isSlotBlocked(slot); + const isEmpty = !instance; + const SlotIcon = SLOT_ICONS[slot]; + + const slotContent = ( + !blocked && onSlotClick(slot)} + onKeyDown={(e) => { + if (!blocked && (e.key === 'Enter' || e.key === ' ')) { + onSlotClick(slot); + } + }} + > +
+
+ + + {SLOT_NAMES[slot]} + + {blocked && ( + + + Occupied — 2H Weapon + + )} +
+ {instance && !blocked && ( + + )} +
+ + {instance ? ( + + ) : blocked ? ( +
+ + Blocked by 2-handed weapon +
+ ) : ( +
+ {SLOT_NAMES[slot]} +
+ )} +
+ ); + + if (blocked) { + return ( + + + + {slotContent} + + +

The offhand slot is blocked because a 2-handed weapon is equipped in the main hand.

+

Unequip the 2-handed weapon to use this slot.

+
+
+
+ ); + } + + return
{slotContent}
; + }; + + return ( +
+ {SLOT_GROUPS.map((group) => ( +
+

+ {group.label} +

+
+ {group.slots.map((slot) => renderSlot(slot))} +
+
+ ))} +
+ ); +} + +interface EquipmentItemDisplayProps { + instance: EquipmentInstance; + equipmentType: EquipmentType | undefined; + isTwoHanded: boolean; + isCompact?: boolean; +} + +function EquipmentItemDisplay({ + instance, + equipmentType, + isTwoHanded, + isCompact = false, +}: EquipmentItemDisplayProps) { + return ( +
+
+ {instance.name} + {isTwoHanded && ( + + 2-Handed + + )} +
+
+ Enchantments: {instance.enchantments.length}/{instance.totalCapacity} +
+ {instance.enchantments.length > 0 && ( + + )} +
+ ); +} + +interface EnchantmentsDisplayProps { + enchantments: Array<{ effectId: string; stacks: number }>; + compact?: boolean; +} + +function EnchantmentsDisplay({ enchantments, compact = false }: EnchantmentsDisplayProps) { + return ( +
+ {enchantments.map((ench, i) => { + const effect = ENCHANTMENT_EFFECTS[ench.effectId]; + return ( + + + + + {effect?.name || ench.effectId} + {ench.stacks > 1 && ` x${ench.stacks}`} + + + +

{effect?.description || 'Unknown effect'}

+

+ Category: {effect?.category || 'unknown'} +

+
+
+
+ ); + })} +
+ ); +} diff --git a/src/components/game/tabs/EquipmentTab.tsx b/src/components/game/tabs/EquipmentTab.tsx index 378da5b..23a5f43 100755 --- a/src/components/game/tabs/EquipmentTab.tsx +++ b/src/components/game/tabs/EquipmentTab.tsx @@ -15,42 +15,13 @@ import { Button } from '@/components/ui/button'; import { GameCard } from '@/components/ui/game-card'; import { SectionHeader } from '@/components/ui/section-header'; import { StatRow } from '@/components/ui/stat-row'; -import { ActionButton } from '@/components/ui/action-button'; import { Badge } from '@/components/ui/badge'; -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '@/components/ui/select'; -import { - Sword, - Shield, - ShieldOff, - Shirt, - Hand, - Footprints, - Gem, - X, - AlertCircle, - Info, - ChevronDown, - HardHat, -} from 'lucide-react'; +import type { GameStore, EquipmentInstance } from '@/lib/game/types'; +import { EquipmentSlotGrid } from './EquipmentSlotGrid'; +import { EquipmentInventory } from './EquipmentInventory'; +import { EnchantmentsPanel } from './EnchantmentsPanel'; import { useGameToast } from '@/components/game/GameToast'; import { ConfirmDialog } from '@/components/game/ConfirmDialog'; -import type { GameStore, EquipmentInstance } from '@/lib/game/types'; - -export interface EquipmentTabProps { - store: GameStore; -} // Slot display names const SLOT_NAMES: Record = { @@ -65,7 +36,7 @@ const SLOT_NAMES: Record = { }; // Rarity color mappings using design system tokens -const RARITY_BORDER_COLORS: Record = { +export const RARITY_BORDER_COLORS: Record = { common: 'border-[var(--text-muted)]', uncommon: 'border-[var(--color-success)]', rare: 'border-[var(--mana-water)]', @@ -74,7 +45,7 @@ const RARITY_BORDER_COLORS: Record = { mythic: 'border-[var(--mana-dark)]', }; -const RARITY_BG_COLORS: Record = { +export const RARITY_BG_COLORS: Record = { common: 'bg-[var(--bg-sunken)]/30', uncommon: 'bg-[var(--color-success)]/10', rare: 'bg-[var(--mana-water)]/10', @@ -83,7 +54,7 @@ const RARITY_BG_COLORS: Record = { mythic: 'bg-[var(--mana-dark)]/10', }; -const RARITY_TEXT_COLORS: Record = { +export const RARITY_TEXT_COLORS: Record = { common: 'text-[var(--text-secondary)]', uncommon: 'text-[var(--color-success)]', rare: 'text-[var(--mana-water)]', @@ -92,19 +63,57 @@ const RARITY_TEXT_COLORS: Record = { mythic: 'text-[var(--mana-dark)]', }; -// Slot icon mapping using Lucide icons const SLOT_ICONS: Record = { - mainHand: Sword, - offHand: Shield, - head: HardHat, - body: Shirt, - hands: Hand, - feet: Footprints, - accessory1: Gem, - accessory2: Gem, + mainHand: () => ( + + + + + ), + offHand: () => ( + + + + ), + head: () => ( + + + + + ), + body: () => ( + + + + + ), + hands: () => ( + + + + + ), + feet: () => ( + + + + + ), + accessory1: () => ( + + + + + ), + accessory2: () => ( + + + + + ), }; -// Slot grouping for visual layout - requirement: visual slot layout +// Slot grouping for visual layout type SlotGroup = { label: string; slots: EquipmentSlot[]; @@ -116,7 +125,7 @@ const SLOT_GROUPS: SlotGroup[] = [ { label: 'Accessories', slots: ['accessory1', 'accessory2'] }, ]; -export function EquipmentTab({ store }: EquipmentTabProps) { +export function EquipmentTab({ store }: { store: GameStore }) { const showToast = useGameToast(); const [selectedSlot, setSelectedSlot] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState<{ instanceId: string; name: string } | null>(null); @@ -147,18 +156,10 @@ export function EquipmentTab({ store }: EquipmentTabProps) { const instanceId = store.equippedInstances[slot]; const instance = instanceId ? store.equipmentInstances[instanceId] : null; store.unequipItem(slot); -showToast('success', 'Item Unequipped', `${instance?.name || 'Item'} removed from ${SLOT_NAMES[slot]}`); + showToast('success', 'Item Unequipped', `${instance?.name || 'Item'} removed from ${SLOT_NAMES[slot]}`); }; - // Get items that can be equipped in a slot - const getEquippableItems = (slot: EquipmentSlot): EquipmentInstance[] => { - const equipmentTypes = getEquipmentBySlot(slot); - const typeIds = new Set(equipmentTypes.map((t) => t.id)); - - return unequippedItems.filter((inst) => typeIds.has(inst.typeId)); - }; - - // Check if a slot is blocked by a 2-handed weapon (task3 bug #6) + // Check if a slot is blocked by a 2-handed weapon const isSlotBlocked = (slot: EquipmentSlot): boolean => { if (slot === 'offHand' && store.equippedInstances.mainHand) { const mainHandInstance = store.equipmentInstances[store.equippedInstances.mainHand]; @@ -169,6 +170,13 @@ showToast('success', 'Item Unequipped', `${instance?.name || 'Item'} removed fro return false; }; + // Get items that can be equipped in a slot + const getEquippableItems = (slot: EquipmentSlot): EquipmentInstance[] => { + const equipmentTypes = getEquipmentBySlot(slot); + const typeIds = new Set(equipmentTypes.map((t) => t.id)); + return unequippedItems.filter((inst) => typeIds.has(inst.typeId)); + }; + // Get all items that can go in a slot const getItemsForSlot = (slot: EquipmentSlot): EquipmentInstance[] => { if (isSlotBlocked(slot)) return []; @@ -177,7 +185,6 @@ showToast('success', 'Item Unequipped', `${instance?.name || 'Item'} removed fro const accessoryTypeIds = Object.values(EQUIPMENT_TYPES) .filter((t) => t.category === 'accessory') .map((t) => t.id); - return unequippedItems.filter((inst) => accessoryTypeIds.includes(inst.typeId)); } @@ -191,145 +198,36 @@ showToast('success', 'Item Unequipped', `${instance?.name || 'Item'} removed fro return getEquippableItems(slot); }; - // Render a single equipment slot - const renderSlot = (slot: EquipmentSlot) => { - const instanceId = store.equippedInstances[slot]; - const instance = instanceId ? store.equipmentInstances[instanceId] : null; - const equipmentType = instance ? EQUIPMENT_TYPES[instance.typeId] : null; - const blocked = isSlotBlocked(slot); - const isEmpty = !instance; - const SlotIcon = SLOT_ICONS[slot]; + // Check if an instance is currently equipped + const isEquipped = (instanceId: string): boolean => + Object.values(store.equippedInstances).includes(instanceId); - const slotContent = ( - -
-
- - - {SLOT_NAMES[slot]} - - {blocked && ( - - - Occupied — 2H Weapon - - )} -
- {instance && !blocked && ( - { - e.stopPropagation(); - handleUnequip(slot); - }} - aria-label={`Unequip ${instance.name}`} - > - - - )} -
- - {instance ? ( -
-
- {instance.name} - {equipmentType?.twoHanded && ( - - 2-Handed - - )} -
-
- Enchantments: {instance.enchantments.length}/{instance.totalCapacity} -
- {instance.enchantments.length > 0 && ( -
- {instance.enchantments.map((ench, i) => { - const effect = ENCHANTMENT_EFFECTS[ench.effectId]; - return ( - - - - - {effect?.name || ench.effectId} - {ench.stacks > 1 && ` x${ench.stacks}`} - - - -

{effect?.description || 'Unknown effect'}

-

- Category: {effect?.category || 'unknown'} -

-
-
-
- ); - })} -
- )} -
- ) : blocked ? ( -
- - Blocked by 2-handed weapon -
- ) : ( -
- {SLOT_NAMES[slot]} -
- )} -
- ); - - if (blocked) { - return ( - - - - {slotContent} - - -

The offhand slot is blocked because a 2-handed weapon is equipped in the main hand.

-

Unequip the 2-handed weapon to use this slot.

-
-
-
- ); + // Get all slots an item type can be equipped to + const getEquippableSlots = (typeId: string): EquipmentSlot[] => { + const equipmentType = EQUIPMENT_TYPES[typeId]; + if (!equipmentType) return []; + if (equipmentType.category === 'accessory') { + return ['accessory1', 'accessory2']; } + return [equipmentType.slot]; + }; - return
{slotContent}
; + // Handle item deletion + const handleDelete = (instanceId: string, name: string) => { + setDeleteConfirm({ instanceId, name }); + }; + + const confirmDelete = () => { + if (deleteConfirm) { + store.deleteEquipmentInstance(deleteConfirm.instanceId); + showToast('success', 'Item Discarded', `${deleteConfirm.name} has been removed from inventory`); + setDeleteConfirm(null); + } }; return (
- {/* Equipment Slots - Requirement: Visual slot layout */} + {/* Equipment Slots */} } /> -
- {/* Render slot groups */} - {SLOT_GROUPS.map((group) => ( -
-

- {group.label} -

-
- {group.slots.map((slot) => renderSlot(slot))} -
-
- ))} -
+
- {/* Inventory */} + {/* Equipment Inventory */} ) : ( -
- {unequippedItems.map((instance) => { - const equipmentType = EQUIPMENT_TYPES[instance.typeId]; - const validSlots = equipmentType - ? (equipmentType.category === 'accessory' - ? ['accessory1', 'accessory2'] as EquipmentSlot[] - : [equipmentType.slot]) - : []; - - return ( - -
-
-
- {instance.name} -
-
- {equipmentType?.description} -
-
- - {equipmentType?.category || 'unknown'} - -
- -
-
- Capacity: {instance.usedCapacity}/{instance.totalCapacity} - {instance.quality < 100 && ( - - (Quality: {instance.quality}%) - - )} -
- {instance.enchantments.length > 0 && ( -
- {instance.enchantments.map((ench, i) => { - const effect = ENCHANTMENT_EFFECTS[ench.effectId]; - return ( - - {effect?.name || ench.effectId} - - ); - })} -
- )} -
- - {validSlots.length > 0 && ( -
- - - - - - setDeleteConfirm({ instanceId: instance.instanceId, name: instance.name })} - aria-label={`Delete ${instance.name}`} - > - - - - -

Delete this item

-
-
-
-
- )} -
- ); - })} -
+ )}
@@ -540,7 +328,7 @@ showToast('success', 'Item Unequipped', `${instance?.name || 'Item'} removed fro })()}
- + {/* Active Effects from Equipment */}
Active Effects from Equipment:
@@ -564,21 +352,15 @@ showToast('success', 'Item Unequipped', `${instance?.name || 'Item'} removed fro {/* Delete Confirmation Dialog */} - {deleteConfirm && ( - setDeleteConfirm(null)} - title="Discard Item?" - description={`Discard ${deleteConfirm.name}? This cannot be undone.`} - variant="danger" - confirmText="Discard" - onConfirm={() => { - store.deleteEquipmentInstance(deleteConfirm.instanceId); - showToast('success', 'Item Discarded', `${deleteConfirm.name} has been removed from inventory`); - setDeleteConfirm(null); - }} - /> - )} + setDeleteConfirm(null)} + title="Discard Item?" + description={`Discard ${deleteConfirm?.name}? This cannot be undone.`} + variant="danger" + confirmText="Discard" + onConfirm={confirmDelete} + />
); }