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

some refactors i guess lol

besaid.zone 1d834f0b 30dc2abf

verified
Changed files
+280 -258
apps
+92
apps/skillulator/src/components/MasterSkillVariationDialog.tsx
··· 1 + import type { RefObject } from "react"; 2 + import { IncreaseSkillToMaxButton, SkillIconButton } from "./action-buttons"; 3 + import { Requirement } from "./Requirement"; 4 + import type { Language, Skill as SkillType } from "@/types"; 5 + 6 + export function MasterSkillVariationDialog(props: { 7 + ref: RefObject<HTMLDialogElement | null>; 8 + masterVariations: SkillType["masterVariations"]; 9 + skill: SkillType; 10 + jobId: number | undefined; 11 + skillId: number; 12 + hasMinLevelRequirements: boolean; 13 + isMaxed: boolean; 14 + lang: Language; 15 + }) { 16 + const skillName = props.skill.name.en 17 + .replaceAll(" ", "") 18 + .replace(/'/, "") 19 + .replace(/[\[\]()]/g, "") 20 + .replace(":", ""); 21 + return ( 22 + <dialog 23 + ref={props.ref} 24 + className="border border-gray-300 shadow-sm rounded-md p-5 w-1/2 xl:w-[60%] mx-auto my-auto" 25 + > 26 + <div className="flex justify-between mb-4"> 27 + <h2 className="font-bold"> 28 + Select a Master Variation (you can only choose one) 29 + </h2> 30 + <button 31 + type="button" 32 + onClick={() => props?.ref?.current?.close()} 33 + className="text-sm bg-red-500 text-white py-1 px-2 rounded-md cursor-pointer" 34 + > 35 + Close Dialog 36 + </button> 37 + </div> 38 + <div className="flex gap-4"> 39 + {props?.masterVariations?.map((variation, index) => { 40 + const selectedMasterVariation = props?.masterVariations?.some( 41 + (s) => s.isSelected === true, 42 + ) 43 + ? props?.masterVariations?.filter((s) => s.isSelected === true) 44 + : props.masterVariations; 45 + 46 + const isSelectedMasterVariation = 47 + !selectedMasterVariation?.every((v) => v.id === variation.id) && 48 + selectedMasterVariation?.length === 1; 49 + 50 + return ( 51 + <div 52 + key={JSON.stringify({ id: variation.id, index })} 53 + data-skill={skillName} 54 + className="relative flex flex-col items-center flex-1 py-2 bg-white border border-gray-300 rounded-md basis-1/2 min-content" 55 + > 56 + <SkillIconButton 57 + locale={props.lang} 58 + skill={{ 59 + ...variation, 60 + id: props.skill.id, 61 + masterVariations: props.masterVariations, 62 + }} 63 + masterVariationSkillId={variation?.id} 64 + isSelectedMasterVariation={isSelectedMasterVariation} 65 + jobId={props.jobId} 66 + hasMinLevelRequirements={variation?.hasMinLevelRequirements} 67 + isMaxed={variation?.isMaxed} 68 + /> 69 + <div> 70 + {variation.requirements.map((variation, index: number) => ( 71 + <Requirement 72 + key={JSON.stringify({ variation, index })} 73 + hasMinLevelRequirements={variation.hasMinLevel} 74 + skill={{ level: variation.level, name: variation.name }} 75 + /> 76 + ))} 77 + </div> 78 + <IncreaseSkillToMaxButton 79 + skillId={props.skill.id} 80 + jobId={props.jobId} 81 + isSelectedMasterVariation={isSelectedMasterVariation} 82 + masterVariationSkillId={variation.id} 83 + hasMinLevelRequirements={variation?.hasMinLevelRequirements} 84 + isMaxed={variation?.isMaxed} 85 + /> 86 + </div> 87 + ); 88 + })} 89 + </div> 90 + </dialog> 91 + ); 92 + }
+75
apps/skillulator/src/components/Skill.tsx
··· 1 + import type { Language, Skill as SkillType } from "@/types"; 2 + import { languages } from "@/utils/constants"; 3 + import { getLanguageForSkill } from "@/utils/language"; 4 + import { IncreaseSkillToMaxButton, SkillIconButton } from "./action-buttons"; 5 + import { Requirement } from "./Requirement"; 6 + import { useRef, type ComponentRef } from "react"; 7 + import { MasterSkillVariationDialog } from "./MasterSkillVariationDialog"; 8 + 9 + interface SkillProps { 10 + skill: SkillType; 11 + jobId: number | undefined; 12 + skillId: number; 13 + hasMinLevelRequirements: boolean; 14 + isMaxed: boolean; 15 + lang: Language; 16 + } 17 + 18 + export default function Skill(props: SkillProps) { 19 + const locale = getLanguageForSkill(languages, props.lang); 20 + const skillName = props.skill.name.en 21 + .replaceAll(" ", "") 22 + .replace(/'/, "") 23 + .replace(/[\[\]()]/g, "") 24 + .replace(":", ""); 25 + 26 + const isMasterVariationSkill = 27 + props?.skill?.masterVariations?.length && 28 + props?.skill?.masterVariations?.length > 1; 29 + 30 + const masterDialogRef = useRef<ComponentRef<"dialog">>(null); 31 + 32 + return ( 33 + <div 34 + data-skill={skillName} 35 + className="scoped-anchor relative flex flex-col items-center flex-1 pt-3 bg-white border border-gray-300 rounded-md basis-1/2 min-content" 36 + > 37 + <SkillIconButton {...props} locale={locale} /> 38 + {props.skill.isPassiveSkill ? ( 39 + <span className="text-sm font-semibold text-purple-600"> 40 + (passive skill) 41 + </span> 42 + ) : null} 43 + <div> 44 + {props.skill.requirements.map((skill, index: number) => ( 45 + <Requirement 46 + key={JSON.stringify({ skill, index })} 47 + hasMinLevelRequirements={skill.hasMinLevel} 48 + skill={{ level: skill.level, name: skill.name }} 49 + /> 50 + ))} 51 + </div> 52 + <div> 53 + <button 54 + type="button" 55 + aria-label="open master variation dialog" 56 + onClick={() => masterDialogRef?.current?.showModal()} 57 + > 58 + {isMasterVariationSkill ? ( 59 + <img 60 + src="https://api.flyff.com/image/badge/master6.png" 61 + alt="" 62 + className="h-6 w-6 absolute top-2 left-3 cursor-pointer" 63 + /> 64 + ) : null} 65 + </button> 66 + <IncreaseSkillToMaxButton {...props} /> 67 + </div> 68 + <MasterSkillVariationDialog 69 + ref={masterDialogRef} 70 + masterVariations={props?.skill?.masterVariations} 71 + {...props} 72 + /> 73 + </div> 74 + ); 75 + }
+20 -15
apps/skillulator/src/hooks/useSkillTree.ts
··· 11 11 import { useCallback, useEffect, useState } from "react"; 12 12 import { useTranslation } from "react-i18next"; 13 13 import { useTreeStore } from "@/zustand/treeStore"; 14 + import type { Language, Skills } from "@/types"; 14 15 15 16 export function useSkillTree() { 16 17 const { i18n } = useTranslation(); ··· 98 99 ]); 99 100 100 101 const sortedSkills = skills 101 - ?.toSorted((a, b) => a.level - b.level) 102 - ?.map((skill) => ({ 103 - ...skill, 104 - hasMinLevelRequirements: checkSkillRequirements(skill), 105 - isMaxed: isSkillMaxed(skill), 106 - masterVariations: skill?.masterVariations 107 - ?.toSorted((a, b) => a.level - b.level) 108 - ?.map((variation) => ({ 109 - ...variation, 110 - hasMinLevelRequirements: checkSkillRequirements(variation), 111 - isMaxed: isSkillMaxed(variation), 112 - isSelected: isMasterSkillSelected(variation), 113 - })), 114 - })); 102 + ? [...skills] 103 + .sort((a, b) => a.level - b.level) 104 + .map((skill) => ({ 105 + ...skill, 106 + hasMinLevelRequirements: checkSkillRequirements(skill), 107 + isMaxed: isSkillMaxed(skill), 108 + masterVariations: skill?.masterVariations 109 + ? [...skill.masterVariations] 110 + .sort((a, b) => a.level - b.level) 111 + .map((variation) => ({ 112 + ...variation, 113 + hasMinLevelRequirements: checkSkillRequirements(variation), 114 + isMaxed: isSkillMaxed(variation), 115 + isSelected: isMasterSkillSelected(variation), 116 + })) 117 + : [], 118 + })) 119 + : []; 115 120 116 121 return { 117 122 job, ··· 119 124 level, 120 125 skillPoints, 121 126 copied, 122 - language: i18n.language, 127 + language: i18n.language as Language, 123 128 handleLevelChange, 124 129 copyToClipboard, 125 130 handleReset,
+8 -2
apps/skillulator/src/index.css
··· 28 28 } 29 29 30 30 .attributes-popover { 31 - inset: auto; 32 - margin: 0; 33 31 position-anchor: --attributes-popover; 34 32 position-area: bottom right; 35 33 position-try-fallbacks: --top-center, --top-left; 34 + } 35 + 36 + body:has(dialog[open]) .skill-icon-anchor { 37 + anchor-name: none !important; 38 + } 39 + 40 + .skill-icon-anchor { 41 + anchor-name: --attributes-popover; 36 42 } 37 43 38 44 @position-try --top-left {
apps/skillulator/src/routes/c/$class/components/Requirement.tsx apps/skillulator/src/components/Requirement.tsx
-158
apps/skillulator/src/routes/c/$class/components/Skill.tsx
··· 1 - import type { Skill as SkillType } from "@/types"; 2 - import { languages } from "@/utils/constants"; 3 - import { getLanguageForSkill } from "@/utils/language"; 4 - import { 5 - // DecreaseSkillPointButton, 6 - IncreaseSkillToMaxButton, 7 - SkillIconButton, 8 - } from "./action-buttons"; 9 - import { Requirement } from "./Requirement"; 10 - import { useRef, type ComponentRef, type RefObject } from "react"; 11 - 12 - interface SkillProps { 13 - skill: SkillType; 14 - jobId: number | undefined; 15 - skillId: number; 16 - hasMinLevelRequirements: boolean; 17 - isMaxed: boolean; 18 - lang: string; 19 - } 20 - 21 - export default function Skill(props: SkillProps) { 22 - const locale = getLanguageForSkill(languages, props.lang); 23 - const skillName = props.skill.name.en 24 - .replaceAll(" ", "") 25 - .replace(/'/, "") 26 - .replace(/[\[\]()]/g, "") 27 - .replace(":", ""); 28 - 29 - const isMasterVariationSkill = 30 - typeof props?.skill?.masterVariations?.length !== "undefined"; 31 - 32 - const masterDialogRef = useRef<ComponentRef<"dialog">>(null); 33 - 34 - return ( 35 - <div 36 - data-skill={skillName} 37 - className="scoped-anchor relative flex flex-col items-center flex-1 pt-3 bg-white border border-gray-300 rounded-md basis-1/2 min-content" 38 - > 39 - <SkillIconButton {...props} locale={locale} /> 40 - {props.skill.isPassiveSkill ? ( 41 - <span className="text-sm font-semibold text-purple-600"> 42 - (passive skill) 43 - </span> 44 - ) : null} 45 - <div> 46 - {props.skill.requirements.map((skill, index: number) => ( 47 - <Requirement 48 - key={JSON.stringify({ skill, index })} 49 - hasMinLevelRequirements={skill.hasMinLevel} 50 - skill={{ level: skill.level, name: skill.name }} 51 - /> 52 - ))} 53 - </div> 54 - <div> 55 - <button 56 - type="button" 57 - aria-label="open master variation dialog" 58 - onClick={() => masterDialogRef?.current?.showModal()} 59 - > 60 - {isMasterVariationSkill ? ( 61 - <img 62 - src="https://api.flyff.com/image/badge/master6.png" 63 - alt="" 64 - className="h-6 w-6 absolute top-2 left-3 cursor-pointer" 65 - /> 66 - ) : null} 67 - </button> 68 - {/* <DecreaseSkillPointButton {...props} /> */} 69 - <IncreaseSkillToMaxButton {...props} /> 70 - </div> 71 - <MasterSkillVariationDialog 72 - ref={masterDialogRef} 73 - masterVariations={props?.skill?.masterVariations} 74 - {...props} 75 - /> 76 - </div> 77 - ); 78 - } 79 - 80 - function MasterSkillVariationDialog(props: { 81 - ref: RefObject<HTMLDialogElement | null>; 82 - masterVariations: SkillType["masterVariations"]; 83 - skill: SkillType; 84 - jobId: number | undefined; 85 - skillId: number; 86 - hasMinLevelRequirements: boolean; 87 - isMaxed: boolean; 88 - lang: string; 89 - }) { 90 - return ( 91 - <dialog 92 - ref={props.ref} 93 - className="border border-gray-300 shadow-sm rounded-md p-5 w-1/2 xl:w-[60%] mx-auto my-auto" 94 - > 95 - <div className="flex justify-between mb-4"> 96 - <h2 className="font-bold"> 97 - Select a Master Variation (you can only choose one) 98 - </h2> 99 - <button 100 - type="button" 101 - onClick={() => props?.ref?.current?.close()} 102 - className="text-sm bg-red-500 text-white py-1 px-2 rounded-md cursor-pointer" 103 - > 104 - Close Dialog 105 - </button> 106 - </div> 107 - <div className="flex gap-4"> 108 - {props?.masterVariations?.map((variation, index) => { 109 - const selectedMasterVariation = props?.masterVariations?.some( 110 - (s) => s.isSelected === true, 111 - ) 112 - ? props?.masterVariations?.filter((s) => s.isSelected === true) 113 - : props.masterVariations; 114 - 115 - const isSelectedMasterVariation = 116 - !selectedMasterVariation?.every((v) => v.id === variation.id) && 117 - selectedMasterVariation?.length === 1; 118 - 119 - return ( 120 - <div 121 - key={JSON.stringify({ id: variation.id, index })} 122 - data-skill={props.skill.name} 123 - className="relative flex flex-col items-center flex-1 py-2 bg-white border border-gray-300 rounded-md basis-1/2 min-content" 124 - > 125 - <SkillIconButton 126 - locale={props.lang} 127 - skill={{ ...variation, id: props.skill.id }} 128 - masterVariationSkillId={variation?.id} 129 - isSelectedMasterVariation={isSelectedMasterVariation} 130 - jobId={props.jobId} 131 - hasMinLevelRequirements={variation?.hasMinLevelRequirements} 132 - isMaxed={variation?.isMaxed} 133 - /> 134 - <div> 135 - {variation.requirements.map((variation, index: number) => ( 136 - <Requirement 137 - key={JSON.stringify({ variation, index })} 138 - hasMinLevelRequirements={variation.hasMinLevel} 139 - skill={{ level: variation.level, name: variation.name }} 140 - /> 141 - ))} 142 - </div> 143 - {/* <DecreaseSkillPointButton {...props} /> */} 144 - <IncreaseSkillToMaxButton 145 - skillId={props.skill.id} 146 - jobId={props.jobId} 147 - isSelectedMasterVariation={isSelectedMasterVariation} 148 - masterVariationSkillId={variation.id} 149 - hasMinLevelRequirements={variation?.hasMinLevelRequirements} 150 - isMaxed={variation?.isMaxed} 151 - /> 152 - </div> 153 - ); 154 - })} 155 - </div> 156 - </dialog> 157 - ); 158 - }
apps/skillulator/src/routes/c/$class/components/Tooltip.tsx apps/skillulator/src/components/Tooltip.tsx
+1 -1
apps/skillulator/src/routes/c/$class/components/action-buttons.tsx apps/skillulator/src/components/action-buttons.tsx
··· 89 89 const popoverRef = useRef<ComponentRef<"div">>(null); 90 90 91 91 return ( 92 - <div style={{ anchorName: "--attributes-popover" }}> 92 + <div className="skill-icon-anchor"> 93 93 {/* biome-ignore lint/a11y/useKeyWithMouseEvents: <explanation> */} 94 94 <button 95 95 type="button"
+80 -80
apps/skillulator/src/routes/c/$class/route.tsx
··· 2 2 import clsx from "clsx"; 3 3 import { t } from "i18next"; 4 4 import { Suspense } from "react"; 5 - import Skill from "./components/Skill"; 5 + import Skill from "../../../components/Skill"; 6 6 import { useSkillTree } from "@/hooks/useSkillTree"; 7 - import { CopyButton, ResetButton } from "./components/action-buttons"; 7 + import { CopyButton, ResetButton } from "../../../components/action-buttons"; 8 8 9 9 export const Route = createFileRoute("/c/$class")({ 10 - component: SkillTree, 10 + component: SkillTree, 11 11 }); 12 12 13 13 function SkillTree() { 14 - const { 15 - job, 16 - skills, 17 - level, 18 - skillPoints, 19 - copied, 20 - language, 21 - handleLevelChange, 22 - copyToClipboard, 23 - handleReset, 24 - } = useSkillTree(); 14 + const { 15 + job, 16 + skills, 17 + level, 18 + skillPoints, 19 + copied, 20 + language, 21 + handleLevelChange, 22 + copyToClipboard, 23 + handleReset, 24 + } = useSkillTree(); 25 25 26 - const params = useParams({ from: "/c/$class" }); 26 + const params = useParams({ from: "/c/$class" }); 27 27 28 - return ( 29 - <> 30 - <Suspense> 31 - <div className="px-5 2xl:px-0 my-10"> 32 - <div className="flex flex-col justify-between mb-10 md:flex-row max-w-[1440px] mx-auto"> 33 - <div className="flex flex-col-reverse"> 34 - <h1 className="text-2xl font-bold capitalize">{params.class}</h1> 35 - <Link 36 - to="/" 37 - className="mb-2 text-indigo-600 hover:underline md:mb-4 a11y-focus" 38 - > 39 - {" "} 40 - &larr; {t("classSelectionLink")} 41 - </Link> 42 - </div> 43 - <div className="flex flex-col gap-2 md:flex-row"> 44 - <div className="flex justify-between gap-2"> 45 - <div className="self-end h-min"> 46 - <p>{t("availSkillPoints")}</p> 47 - <span className="font-bold">{skillPoints}</span> 48 - </div> 49 - <div className="flex flex-col self-end h-min"> 50 - <label htmlFor="characterLevel" className="text-sm"> 51 - {t("charLevel")} 52 - </label> 53 - <input 54 - type="number" 55 - className="rounded-md border border-gray-300 px-4 py-1.5 a11y-focus" 56 - inputMode="numeric" 57 - pattern="[0-9]*" 58 - value={level} 59 - onChange={handleLevelChange} 60 - min={15} 61 - max={190} 62 - /> 63 - </div> 64 - </div> 65 - <CopyButton copied={copied} copyToClipboard={copyToClipboard} /> 66 - <ResetButton handleReset={handleReset} /> 67 - </div> 68 - </div> 69 - <div 70 - className={clsx( 71 - "xl:grid max-w-[1440px] mx-auto gap-0.5", 72 - params.class, 73 - )} 74 - > 75 - {skills?.map((skill, index: number) => { 76 - return ( 77 - <Skill 78 - lang={language} 79 - key={JSON.stringify({ name: skill.name, index })} 80 - hasMinLevelRequirements={skill.hasMinLevelRequirements} 81 - isMaxed={skill.isMaxed} 82 - skill={skill} 83 - skillId={skill.id} 84 - jobId={job?.id} 85 - /> 86 - ); 87 - })} 88 - </div> 89 - </div> 90 - </Suspense> 91 - </> 92 - ); 28 + return ( 29 + <> 30 + <Suspense> 31 + <div className="px-5 2xl:px-0 my-10"> 32 + <div className="flex flex-col justify-between mb-10 md:flex-row max-w-[1440px] mx-auto"> 33 + <div className="flex flex-col-reverse"> 34 + <h1 className="text-2xl font-bold capitalize">{params.class}</h1> 35 + <Link 36 + to="/" 37 + className="mb-2 text-indigo-600 hover:underline md:mb-4 a11y-focus" 38 + > 39 + {" "} 40 + &larr; {t("classSelectionLink")} 41 + </Link> 42 + </div> 43 + <div className="flex flex-col gap-2 md:flex-row"> 44 + <div className="flex justify-between gap-2"> 45 + <div className="self-end h-min"> 46 + <p>{t("availSkillPoints")}</p> 47 + <span className="font-bold">{skillPoints}</span> 48 + </div> 49 + <div className="flex flex-col self-end h-min"> 50 + <label htmlFor="characterLevel" className="text-sm"> 51 + {t("charLevel")} 52 + </label> 53 + <input 54 + type="number" 55 + className="rounded-md border border-gray-300 px-4 py-1.5 a11y-focus" 56 + inputMode="numeric" 57 + pattern="[0-9]*" 58 + value={level} 59 + onChange={handleLevelChange} 60 + min={15} 61 + max={190} 62 + /> 63 + </div> 64 + </div> 65 + <CopyButton copied={copied} copyToClipboard={copyToClipboard} /> 66 + <ResetButton handleReset={handleReset} /> 67 + </div> 68 + </div> 69 + <div 70 + className={clsx( 71 + "xl:grid max-w-[1440px] mx-auto gap-0.5", 72 + params.class, 73 + )} 74 + > 75 + {skills?.map((skill, index: number) => { 76 + return ( 77 + <Skill 78 + lang={language} 79 + key={JSON.stringify({ name: skill.name, index })} 80 + hasMinLevelRequirements={skill.hasMinLevelRequirements} 81 + isMaxed={skill.isMaxed} 82 + skill={skill} 83 + skillId={skill.id} 84 + jobId={job?.id} 85 + /> 86 + ); 87 + })} 88 + </div> 89 + </div> 90 + </Suspense> 91 + </> 92 + ); 93 93 }
apps/skillulator/src/routes/index/components/LinkCard.tsx apps/skillulator/src/components/LinkCard.tsx
+1 -1
apps/skillulator/src/routes/index/route.lazy.tsx
··· 2 2 import { useTranslation } from "react-i18next"; 3 3 import { JOBS } from "@/utils/constants"; 4 4 import { createLazyFileRoute } from "@tanstack/react-router"; 5 - import { LinkCard } from "./components/LinkCard"; 5 + import { LinkCard } from "@/components/LinkCard"; 6 6 7 7 export const Route = createLazyFileRoute("/")({ 8 8 component: Index,
+3 -1
apps/skillulator/src/utils/language.ts
··· 1 + import type { Language } from "@/types"; 1 2 import type { languages } from "./constants"; 2 3 3 4 export function getLanguageForSkill( 4 5 langs: typeof languages, 5 6 appLanguage: string, 6 7 ) { 7 - return langs.find((lang) => lang.label === appLanguage)?.value ?? "en"; 8 + return (langs.find((lang) => lang.label === appLanguage)?.value ?? 9 + "en") as Language; 8 10 }