+117
apps/skillulator/src/hooks/useSkillTree.ts
+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
+1
-1
apps/skillulator/src/routes/c/$class/components/Skill.tsx
+30
-109
apps/skillulator/src/routes/c/$class/route.tsx
+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>