a collection of tools for fly for fun universe skillulator.lol

useSkillTree hook

besaid.zone e16ca40e fbf2ea3b

verified
Changed files
+159 -114
apps
skillulator
src
hooks
routes
c
+117
apps/skillulator/src/hooks/useSkillTree.ts
··· 1 + import { 2 + checkSkillRequirements, 3 + decodeTree, 4 + encodeTree, 5 + getJobByName, 6 + isSkillMaxed, 7 + } from "@/utils/skill-tree-helpers"; 8 + import { useNavigate, useParams } from "@tanstack/react-router"; 9 + import lzstring from "lz-string"; 10 + import { useCallback, useEffect, useState } from "react"; 11 + import { useTranslation } from "react-i18next"; 12 + import { useTreeStore } from "@/zustand/treeStore"; 13 + 14 + export function useSkillTree() { 15 + const { i18n } = useTranslation(); 16 + const params = useParams({ from: "/c/$class" }); 17 + const navigate = useNavigate(); 18 + 19 + const [copied, setCopied] = useState(false); 20 + const [level, setLevel] = useState(15); 21 + 22 + const { 23 + createPreloadedSkillTree, 24 + jobTree, 25 + initSkillPoints, 26 + skillPoints, 27 + resetSkillTree, 28 + } = useTreeStore((state) => state); 29 + const job = getJobByName(params.class, jobTree); 30 + const jobId = job?.id; 31 + const skills = job?.skills; 32 + 33 + const handleLevelChange = useCallback( 34 + (event: React.ChangeEvent<HTMLInputElement>) => { 35 + if (!jobId) return; 36 + const newLevel = +event.target.value; 37 + initSkillPoints(jobId, newLevel); 38 + setLevel(newLevel); 39 + }, 40 + [jobId, initSkillPoints], 41 + ); 42 + 43 + const copyToClipboard = useCallback(async () => { 44 + if (!jobId || !skills) return; 45 + 46 + let treeCode = `${window.location.origin}/c/${params.class}`; 47 + const treeMap = encodeTree(skills, level); 48 + const encodedTree = lzstring.compressToEncodedURIComponent(treeMap); 49 + treeCode += `?tree=${encodedTree}`; 50 + 51 + try { 52 + if (navigator.clipboard && !copied) { 53 + await navigator.clipboard.writeText(treeCode); 54 + setCopied(true); 55 + window.setTimeout(() => setCopied(false), 3000); 56 + } 57 + } catch (e) { 58 + console.error("copyToClipboard", e); 59 + setCopied(false); 60 + } 61 + }, [params.class, copied, jobId, level, skills]); 62 + 63 + const handleReset = useCallback(() => { 64 + if (!jobId) return; 65 + resetSkillTree(jobId); 66 + setLevel(15); 67 + }, [jobId, resetSkillTree]); 68 + 69 + useEffect(() => { 70 + if (!jobId) return; 71 + 72 + const code = new URLSearchParams(window.location.search).get("tree") ?? ""; 73 + if (!code) { 74 + initSkillPoints(jobId, level); 75 + return; 76 + } 77 + 78 + const decompressedCode = lzstring.decompressFromEncodedURIComponent(code); 79 + if (!decompressedCode) { 80 + alert("Error: Invalid tree code!"); 81 + navigate({ to: `/c/${params.class}` }); 82 + return; 83 + } 84 + 85 + const { normalizedSkillMap, characterLevel } = decodeTree(decompressedCode); 86 + setLevel(Number(characterLevel)); 87 + createPreloadedSkillTree(jobId, normalizedSkillMap); 88 + initSkillPoints(jobId, Number(characterLevel)); 89 + }, [ 90 + jobId, 91 + level, 92 + initSkillPoints, 93 + createPreloadedSkillTree, 94 + navigate, 95 + params.class, 96 + ]); 97 + 98 + const sortedSkills = skills 99 + ?.toSorted((a, b) => a.level - b.level) 100 + .map((skill) => ({ 101 + ...skill, 102 + hasMinLevelRequirements: checkSkillRequirements(skill), 103 + isMaxed: isSkillMaxed(skill), 104 + })); 105 + 106 + return { 107 + job, 108 + skills: sortedSkills, 109 + level, 110 + skillPoints, 111 + copied, 112 + language: i18n.language, 113 + handleLevelChange, 114 + copyToClipboard, 115 + handleReset, 116 + }; 117 + }
+1 -1
apps/skillulator/src/routes/c/$class/components/Skill.tsx
··· 10 10 11 11 interface SkillProps { 12 12 skill: SkillType; 13 - jobId: number; 13 + jobId: number | undefined; 14 14 skillId: number; 15 15 hasMinLevelRequirements: boolean; 16 16 isMaxed: boolean;
+11 -4
apps/skillulator/src/routes/c/$class/components/action-buttons.tsx
··· 4 4 5 5 export function DecreaseSkillPointButton(props: { 6 6 skill: Skill; 7 - jobId: number; 7 + jobId: number | undefined; 8 8 }) { 9 9 const { decreaseSkillPoint } = useTreeStore(); 10 10 return ( ··· 16 16 )} 17 17 disabled={props.skill.skillLevel === 0} 18 18 onClick={(event) => { 19 + if (!props.jobId) return; 19 20 event.preventDefault(); 20 21 if (event.type === "click") { 21 22 decreaseSkillPoint(props.jobId, props.skill.id); ··· 30 31 export function IncreaseSkillToMaxButton(props: { 31 32 hasMinLevelRequirements: boolean; 32 33 isMaxed: boolean; 33 - jobId: number; 34 + jobId: number | undefined; 34 35 skillId: number; 35 36 }) { 36 37 const { increaseSkillToMax } = useTreeStore(); ··· 44 45 ? "grayscale-0" 45 46 : "grayscale", 46 47 )} 47 - onClick={() => increaseSkillToMax(props.skillId, props.jobId)} 48 + onClick={() => { 49 + if (!props.jobId) return; 50 + increaseSkillToMax(props.skillId, props.jobId); 51 + }} 48 52 > 49 53 max 50 54 </button> ··· 52 56 } 53 57 54 58 export function SkillIconButton(props: { 55 - jobId: number; 59 + jobId: number | undefined; 56 60 skill: Skill; 57 61 hasMinLevelRequirements: boolean; 58 62 isMaxed: boolean; ··· 64 68 type="button" 65 69 onKeyDown={(event) => { 66 70 if (["ArrowDown", "ArrowUp"].includes(event.key)) { 71 + if (!props.jobId) return; 67 72 event.preventDefault(); 68 73 if (event.key === "ArrowDown") { 69 74 decreaseSkillPoint(props.jobId, props.skill.id); ··· 73 78 } 74 79 }} 75 80 onClick={(event) => { 81 + if (!props.jobId) return; 76 82 event.preventDefault(); 77 83 if (event.type === "click") { 78 84 increaseSkillPoint(props.jobId, props.skill.id); 79 85 } 80 86 }} 81 87 onContextMenu={(event) => { 88 + if (!props.jobId) return; 82 89 event.preventDefault(); 83 90 decreaseSkillPoint(props.jobId, props.skill.id); 84 91 }}
+30 -109
apps/skillulator/src/routes/c/$class/route.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { Link, useParams } from "@tanstack/react-router"; 2 3 import clsx from "clsx"; 3 - import lzstring from "lz-string"; 4 - import { 5 - type ChangeEvent, 6 - Suspense, 7 - useCallback, 8 - useEffect, 9 - useState, 10 - } from "react"; 11 - import { useTranslation } from "react-i18next"; 12 - import { Link, useNavigate, useParams } from "@tanstack/react-router"; 4 + import { t } from "i18next"; 5 + import { Suspense } from "react"; 13 6 import Skill from "./components/Skill"; 14 - import { 15 - decodeTree, 16 - encodeTree, 17 - getJobByName, 18 - } from "@/utils/skill-tree-helpers"; 19 - import { useTreeStore } from "@/zustand/treeStore"; 20 - import { t } from "i18next"; 7 + import { useSkillTree } from "@/hooks/useSkillTree"; 21 8 22 9 export const Route = createFileRoute("/c/$class")({ 23 10 component: SkillTree, 24 11 }); 25 12 26 13 function SkillTree() { 27 - const jobTree = useTreeStore((state) => state.jobTree); 28 - const createPreloadedSkillTree = useTreeStore( 29 - (state) => state.createPreloadedSkillTree, 30 - ); 31 - const initSkillPoints = useTreeStore((state) => state.initSkillPoints); 32 - const skillPoints = useTreeStore((state) => state.skillPoints); 33 - const resetSkillTree = useTreeStore((state) => state.resetSkillTree); 34 - 35 - let params = useParams({ from: "/c/$class" }); 36 - const navigate = useNavigate(); 37 - const skills = getJobByName( 38 - params.class!, 39 - useTreeStore.getState().jobTree, 40 - )?.skills; 41 - 42 - const [copied, setCopied] = useState(false); 43 - const [level, setLevel] = useState(15); 14 + const { 15 + job, 16 + skills, 17 + level, 18 + skillPoints, 19 + copied, 20 + language, 21 + handleLevelChange, 22 + copyToClipboard, 23 + handleReset, 24 + } = useSkillTree(); 44 25 45 - const jobId = getJobByName(params.class!, jobTree)?.id; 46 - 47 - const handleLevelChange = useCallback( 48 - (event: ChangeEvent<HTMLInputElement>) => { 49 - initSkillPoints(jobId!, +event.target.value); 50 - setLevel(+event.target.value); 51 - }, 52 - [], 53 - ); 54 - 55 - const copyToClipboard = useCallback(async () => { 56 - let treeCode = `${window.location.origin}/c/${params.class}`; 57 - if (jobId) { 58 - const treeMap = encodeTree(skills!, level); 59 - const encondedTree = lzstring.compressToEncodedURIComponent(treeMap!); 60 - treeCode += `?tree=${encondedTree}`; 61 - } 62 - 63 - try { 64 - if (navigator.clipboard && !copied) { 65 - await navigator.clipboard.writeText(treeCode); 66 - setCopied(true); 67 - window.setTimeout(() => setCopied(false), 3000); 68 - } 69 - } catch (e) { 70 - console.error("copyToClipboard", e); 71 - setCopied(false); 72 - } 73 - }, [params.class, copied, jobId, level, skills]); 74 - 75 - useEffect(() => { 76 - const code = new URLSearchParams(window.location.search).get("tree") ?? ""; 77 - if (!code) { 78 - initSkillPoints(jobId!, level); 79 - return; 80 - } 81 - 82 - const decompressedCode = lzstring.decompressFromEncodedURIComponent(code); 83 - if (!decompressedCode) { 84 - alert("Error: Invalid tree code!"); 85 - navigate({ to: `/c/${params.class}` }); 86 - return; 87 - } 88 - const { untangledSkillMap, characterLevel } = decodeTree(decompressedCode); 89 - setLevel(+characterLevel!); 90 - 91 - createPreloadedSkillTree(jobId!, untangledSkillMap); 92 - 93 - initSkillPoints(jobId!, +characterLevel!); 94 - }, []); 95 - 96 - const { i18n } = useTranslation(); 26 + const params = useParams({ from: "/c/$class" }); 97 27 98 28 return ( 99 29 <> ··· 146 76 <button 147 77 type="button" 148 78 className="h-min w-full self-end rounded-md border border-red-300 bg-red-100 px-4 py-1.5 text-red-900 duration-150 hover:bg-red-200 md:w-max a11y-focus" 149 - onClick={() => { 150 - resetSkillTree(jobId!); 151 - setLevel(15); 152 - }} 79 + onClick={handleReset} 153 80 > 154 81 {t("resetText")} 155 82 </button> ··· 161 88 params.class, 162 89 )} 163 90 > 164 - {skills 165 - ?.toSorted((a, b) => a.level - b.level) 166 - ?.map((skill) => { 167 - const hasMinLevelRequirements = skill.requirements.every( 168 - (req: any) => req.hasMinLevel === true, 169 - ); 170 - const isMaxed = skill.skillLevel === skill.levels; 171 - return ( 172 - <Skill 173 - lang={i18n.language} 174 - key={skill.id} 175 - hasMinLevelRequirements={hasMinLevelRequirements} 176 - isMaxed={isMaxed} 177 - skill={skill} 178 - skillId={skill.id} 179 - jobId={jobId} 180 - /> 181 - ); 182 - })} 91 + {skills?.map((skill) => { 92 + return ( 93 + <Skill 94 + lang={language} 95 + key={skill.id} 96 + hasMinLevelRequirements={skill.hasMinLevelRequirements} 97 + isMaxed={skill.isMaxed} 98 + skill={skill} 99 + skillId={skill.id} 100 + jobId={job?.id} 101 + /> 102 + ); 103 + })} 183 104 </div> 184 105 </div> 185 106 </Suspense>