feat: enhance SkillTreeModal with skill allocation logic and custom scrollbar styles
This commit is contained in:
parent
243c50a1a0
commit
78c0f52c39
@ -107,7 +107,10 @@ export default function PetDisplay({ pet, onPetUpdate }: PetDisplayProps) {
|
|||||||
)}
|
)}
|
||||||
{showSkillTreeModal && (
|
{showSkillTreeModal && (
|
||||||
<SkillTreeModal
|
<SkillTreeModal
|
||||||
onClose={() => setShowSkillTreeModal(false)}
|
petId={pet.id}
|
||||||
|
onPetUpdate={onPetUpdate}
|
||||||
|
onClose={() => setShowSkillTreeModal(false)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -35,7 +35,7 @@ export default function InventoryModal({ inventory, petId, onClose, onPetUpdate
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||||
<div className="bg-gray-800 p-6 rounded-xl max-w-2xl w-full">
|
<div className="bg-gray-800 p-6 rounded-xl max-w-2xl w-full">
|
||||||
<div className="flex justify-between items-center mb-4">
|
<div className="flex justify-between items-center mb-4">
|
||||||
<h2 className="text-2xl font-bold">Inventory</h2>
|
<h2 className="text-2xl font-bold">Inventory</h2>
|
||||||
|
22
src/components/modal/SkillTreeModal.module.css
Normal file
22
src/components/modal/SkillTreeModal.module.css
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
.customScroll {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #4B5563 #1F2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customScroll::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customScroll::-webkit-scrollbar-track {
|
||||||
|
background: #1F2937;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customScroll::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #4B5563;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.customScroll::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #6B7280;
|
||||||
|
}
|
@ -1,17 +1,180 @@
|
|||||||
import React from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Pet } from '../../types/Pet';
|
||||||
|
import { Skill, PetSkill, SkillType } from '../../types/Skills';
|
||||||
|
import { fetchPets, getAllSkills, getPetSkills, postAllocatePetSkill } from '../../services/api/api';
|
||||||
|
import { Loader2 } from 'lucide-react';
|
||||||
|
import styles from './SkillTreeModal.module.css';
|
||||||
|
|
||||||
interface SkillTreeModalProps {
|
interface SkillTreeModalProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
petId: string;
|
||||||
|
onPetUpdate: (updatedPet: Pet) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SkillTreeModal({ onClose }: SkillTreeModalProps) {
|
export default function SkillTreeModal({ onClose, petId, onPetUpdate }: SkillTreeModalProps) {
|
||||||
|
const [skills, setSkills] = useState<Skill[]>([]);
|
||||||
|
const [petSkills, setPetSkills] = useState<PetSkill[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [allocating, setAllocating] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchData = async () => {
|
||||||
|
try {
|
||||||
|
const [allSkills, ownedSkills] = await Promise.all([
|
||||||
|
getAllSkills(),
|
||||||
|
getPetSkills(petId)
|
||||||
|
]);
|
||||||
|
setSkills(allSkills);
|
||||||
|
setPetSkills(ownedSkills);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching skills:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchData();
|
||||||
|
}, [petId]);
|
||||||
|
|
||||||
|
const handleAllocateSkill = async (skillId: number) => {
|
||||||
|
try {
|
||||||
|
setAllocating(true);
|
||||||
|
const updatedPetSkill = await postAllocatePetSkill(petId, skillId);
|
||||||
|
setPetSkills([...petSkills, updatedPetSkill]);
|
||||||
|
// Refresh pet data to update skill points
|
||||||
|
const updatedPet = await fetchPets();
|
||||||
|
onPetUpdate(updatedPet[0]);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error allocating skill:', error);
|
||||||
|
} finally {
|
||||||
|
setAllocating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSkillTier = (skillId: number) => {
|
||||||
|
return petSkills.find(ps => ps.skillId === skillId)?.currentTier;
|
||||||
|
};
|
||||||
|
|
||||||
|
const canAllocateSkill = (skill: Skill) => {
|
||||||
|
if (!skill.skillsIdRequired) return true;
|
||||||
|
return petSkills.some(ps => ps.skillId === skill.skillsIdRequired![0]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRequiredSkillName = (skillId: number[] | null) => {
|
||||||
|
if (!skillId) return null;
|
||||||
|
return skills.find(s => s.id === skillId[0])?.name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSkillNode = (skill: Skill) => {
|
||||||
|
console.log('Rendering skill:', skill);
|
||||||
|
|
||||||
|
const owned = petSkills.some(ps => ps.skillId === skill.id);
|
||||||
|
const tier = getSkillTier(skill.id);
|
||||||
|
const canAllocate = canAllocateSkill(skill);
|
||||||
|
const requiredSkillName = getRequiredSkillName(skill.skillsIdRequired);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={skill.id}
|
||||||
|
className={`
|
||||||
|
relative p-4 rounded-lg border-2
|
||||||
|
${owned ? 'border-green-500 bg-gray-700' : 'border-gray-600 bg-gray-800'}
|
||||||
|
${!owned && !canAllocate ? 'opacity-50' : ''}
|
||||||
|
hover:border-blue-500 transition-colors
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
{/* <img src={skill.icon} alt={skill.name} className="w-6 h-6" /> */}
|
||||||
|
<span>{skill.icon}</span>
|
||||||
|
<h3 className="font-bold">{skill.name}</h3>
|
||||||
|
{tier && (
|
||||||
|
<span className="px-2 py-1 bg-blue-600 rounded text-xs">
|
||||||
|
Tier {tier}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-300 mb-2">{skill.description}</p>
|
||||||
|
{!canAllocate && requiredSkillName && (
|
||||||
|
<p className="text-red-500 text-sm mb-2">Required skill: {requiredSkillName}</p>
|
||||||
|
)}
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
{skill.effects.map((effect, idx) => (
|
||||||
|
<div key={idx}>
|
||||||
|
Tier {effect.tier}: {effect.effect} ({effect.value})
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{!owned && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleAllocateSkill(skill.id)}
|
||||||
|
disabled={!canAllocate || allocating}
|
||||||
|
className={`
|
||||||
|
mt-2 px-3 py-1 rounded text-sm
|
||||||
|
${canAllocate
|
||||||
|
? 'bg-blue-600 hover:bg-blue-700'
|
||||||
|
: 'bg-gray-600 cursor-not-allowed'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{allocating ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
`Learn (${skill.pointsCost} SP)`
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{owned && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleAllocateSkill(skill.id)}
|
||||||
|
disabled={!canAllocate || allocating}
|
||||||
|
className={`
|
||||||
|
mt-2 px-3 py-1 rounded text-sm
|
||||||
|
${canAllocate
|
||||||
|
? 'bg-green-600 hover:bg-green-700'
|
||||||
|
: 'bg-gray-600 cursor-not-allowed'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{allocating ? (
|
||||||
|
<Loader2 className="w-4 h-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
`Upgrade (${skill.pointsCost} SP)`
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderSkillTree = () => {
|
||||||
|
const groundSkills = skills.filter(s => s.type === 'GROUND');
|
||||||
|
const grandSkills = skills.filter(s => s.type === 'GRAND');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{groundSkills.map(renderSkillNode)}
|
||||||
|
</div>
|
||||||
|
<div className="border-t border-gray-600 pt-6">
|
||||||
|
<h2 className="text-xl font-bold mb-4">Advanced Skills</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{grandSkills.map(renderSkillNode)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center p-4 z-50">
|
||||||
className="fixed inset-0 bg-black/80 backdrop-blur-sm flex items-center justify-center p-4 z-50"
|
<div className={`bg-gray-800 p-6 rounded-xl max-w-4xl w-full max-h-[90vh] overflow-y-auto ${styles.customScroll}`}>
|
||||||
>
|
|
||||||
<div className="bg-gray-800 p-6 rounded-xl max-w-2xl w-full">
|
|
||||||
<h2 className="text-2xl font-bold mb-4">Skill Tree</h2>
|
<h2 className="text-2xl font-bold mb-4">Skill Tree</h2>
|
||||||
<p className="text-gray-400 mb-6">Skill tree modal under development.</p>
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center items-center h-40">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
renderSkillTree()
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="mt-6 flex justify-end">
|
<div className="mt-6 flex justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
|
@ -38,7 +38,7 @@ export async function getAllSkills(): Promise<Skill[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getPetSkills(petId: string): Promise<PetSkill[]> {
|
export async function getPetSkills(petId: string): Promise<PetSkill[]> {
|
||||||
const response = await api.get<PetSkill[]>(`/api/v1/skill/${petId}/pet`);
|
const response = await api.get<PetSkill[]>(`/api/v1/skill/${petId}/skills`);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,26 +1,29 @@
|
|||||||
|
export type SkillType = 'GROUND' | 'GRAND';
|
||||||
|
|
||||||
export interface Skill {
|
export interface Skill {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
type: string;
|
type: SkillType;
|
||||||
pointsCost: number;
|
pointsCost: number;
|
||||||
icon: string;
|
icon: string;
|
||||||
skillsIdRequired: number | null;
|
skillsIdRequired: number[] | null;
|
||||||
effects: SkillEffect[];
|
effects: SkillEffect[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SkillEffect {
|
export interface SkillEffect {
|
||||||
id: number;
|
id: number;
|
||||||
skillId: number;
|
skillId: number;
|
||||||
tier: string;
|
tier: SkillTier;
|
||||||
effect: string;
|
effect: string;
|
||||||
value: number;
|
value: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SkillTier = 'I' | 'II' | 'III';
|
||||||
export interface PetSkill {
|
export interface PetSkill {
|
||||||
id: number;
|
id: number;
|
||||||
petId: string;
|
petId: string;
|
||||||
skillId: number;
|
skillId: number;
|
||||||
skill: Skill;
|
skill: Skill;
|
||||||
currentTier: string;
|
currentTier: SkillTier;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user