refactor: extract components from EquipmentTab.tsx to reduce below 400 lines
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m12s
Build and Publish Mana Loop Docker Image / build-and-publish (push) Failing after 1m12s
This commit is contained in:
@@ -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<EquipmentSlot, React.ElementType> = {
|
||||
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 = (
|
||||
<GameCard
|
||||
variant={blocked ? 'danger' : instance ? 'default' : 'sunken'}
|
||||
className={`relative transition-all duration-200
|
||||
${isEmpty && !blocked ? 'border-dashed' : ''}
|
||||
${blocked ? 'opacity-60 cursor-not-allowed' : 'hover:border-[var(--border-default)]'}
|
||||
`}
|
||||
role="button"
|
||||
aria-label={`${SLOT_NAMES[slot]} slot${blocked ? ' (blocked by 2-handed weapon)' : ''}${instance ? `: ${instance.name}` : ' (empty)'}`}
|
||||
tabIndex={blocked ? -1 : 0}
|
||||
onClick={() => !blocked && onSlotClick(slot)}
|
||||
onKeyDown={(e) => {
|
||||
if (!blocked && (e.key === 'Enter' || e.key === ' ')) {
|
||||
onSlotClick(slot);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<SlotIcon
|
||||
size={16}
|
||||
className={blocked ? 'text-[var(--text-disabled)]' : 'text-[var(--text-secondary)]'}
|
||||
/>
|
||||
<span
|
||||
className={`text-sm font-semibold
|
||||
${blocked ? 'text-[var(--text-disabled)]' : 'text-[var(--text-primary)]'}
|
||||
`}
|
||||
>
|
||||
{SLOT_NAMES[slot]}
|
||||
</span>
|
||||
{blocked && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs border-[var(--mana-dark)] text-[var(--mana-dark)] ml-2"
|
||||
>
|
||||
<AlertCircle size={12} className="mr-1" />
|
||||
Occupied — 2H Weapon
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{instance && !blocked && (
|
||||
<button
|
||||
className="h-6 w-6 p-0 text-[var(--color-danger)] hover:text-[var(--interactive-danger-hover)] hover:bg-[var(--interactive-danger)]/20 rounded flex items-center justify-center"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onUnequip(slot);
|
||||
}}
|
||||
aria-label={`Unequip ${instance.name}`}
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{instance ? (
|
||||
<EquipmentItemDisplay
|
||||
instance={instance}
|
||||
equipmentType={equipmentType}
|
||||
isTwoHanded={equipmentType?.twoHanded || false}
|
||||
isCompact={true}
|
||||
/>
|
||||
) : blocked ? (
|
||||
<div className="text-sm text-[var(--text-disabled)] italic">
|
||||
<AlertCircle size={14} className="inline mr-1" />
|
||||
Blocked by 2-handed weapon
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-[var(--text-muted)] italic text-center py-2">
|
||||
{SLOT_NAMES[slot]}
|
||||
</div>
|
||||
)}
|
||||
</GameCard>
|
||||
);
|
||||
|
||||
if (blocked) {
|
||||
return (
|
||||
<TooltipProvider key={slot}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
{slotContent}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
||||
<p>The offhand slot is blocked because a 2-handed weapon is equipped in the main hand.</p>
|
||||
<p className="text-[var(--text-muted)] text-xs mt-1">Unequip the 2-handed weapon to use this slot.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return <div key={slot}>{slotContent}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{SLOT_GROUPS.map((group) => (
|
||||
<div key={group.label}>
|
||||
<h4 className="text-xs font-semibold text-[var(--text-muted)] uppercase tracking-wider mb-2">
|
||||
{group.label}
|
||||
</h4>
|
||||
<div className={`grid gap-3
|
||||
grid-cols-2
|
||||
${group.slots.includes('mainHand' as EquipmentSlot) ? 'sm:grid-cols-2' : 'sm:grid-cols-2 lg:grid-cols-4'}
|
||||
`}>
|
||||
{group.slots.map((slot) => renderSlot(slot))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface EquipmentItemDisplayProps {
|
||||
instance: EquipmentInstance;
|
||||
equipmentType: EquipmentType | undefined;
|
||||
isTwoHanded: boolean;
|
||||
isCompact?: boolean;
|
||||
}
|
||||
|
||||
function EquipmentItemDisplay({
|
||||
instance,
|
||||
equipmentType,
|
||||
isTwoHanded,
|
||||
isCompact = false,
|
||||
}: EquipmentItemDisplayProps) {
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className={`font-semibold text-sm ${RARITY_TEXT_COLORS[instance.rarity] || 'text-[var(--text-primary)]'}`}>
|
||||
{instance.name}
|
||||
{isTwoHanded && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="ml-2 text-xs border-[var(--mana-light)] text-[var(--mana-light)]"
|
||||
>
|
||||
2-Handed
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--text-secondary)]">
|
||||
Enchantments: {instance.enchantments.length}/{instance.totalCapacity}
|
||||
</div>
|
||||
{instance.enchantments.length > 0 && (
|
||||
<EnchantmentsDisplay enchantments={instance.enchantments} compact={isCompact} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface EnchantmentsDisplayProps {
|
||||
enchantments: Array<{ effectId: string; stacks: number }>;
|
||||
compact?: boolean;
|
||||
}
|
||||
|
||||
function EnchantmentsDisplay({ enchantments, compact = false }: EnchantmentsDisplayProps) {
|
||||
return (
|
||||
<div className={`flex flex-wrap gap-1 ${compact ? 'mt-1' : ''}`}>
|
||||
{enchantments.map((ench, i) => {
|
||||
const effect = ENCHANTMENT_EFFECTS[ench.effectId];
|
||||
return (
|
||||
<TooltipProvider key={i}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-xs cursor-help border-[var(--border-default)] text-[var(--text-secondary)]"
|
||||
>
|
||||
{effect?.name || ench.effectId}
|
||||
{ench.stacks > 1 && ` x${ench.stacks}`}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-[var(--bg-elevated)] border-[var(--border-default)] text-[var(--text-primary)]">
|
||||
<p>{effect?.description || 'Unknown effect'}</p>
|
||||
<p className="text-[var(--text-muted)] text-xs">
|
||||
Category: {effect?.category || 'unknown'}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user