hensei-web/components/job/JobSection/index.tsx
Justin Edmund 739c72f4e0
Add ability to remove job skills (#317)
* Add Awakening type and remove old defs

We remove the flat list of awakening data, as we will be pulling data from the database

* Update types to use new Awakening type

* Update WeaponUnit for Grand weapon awakenings

* Update object modals

We needed to update CharacterModal and WeaponModal to display awakenings from the new data format. However, the component used (`SelectWithInput`) was tied to AX Skills in a way that would take exponentially more time to resolve.

Instead, we forked `SelectWithInput` into `AwakeningSelectWithInput` and did our work there.

`AwakeningSelect` was found to be redundant, so it was removed.

* Update hovercards

* Add max-height to Select

* Allow styling of Select modal with className prop

* Add Job class to Job select

* Add localizations for removing job skills

* Add endpoint for removing job skills

* Implement removing job skills

We added a (...) button next to each editable job skill that opens a context menu that will allow the user to remove the job skill. An alert is presented to make sure the user is sure before proceeding.

As part of this change, some minor restyling of JobSkillItem was necessary
2023-06-19 02:39:27 -07:00

232 lines
6 KiB
TypeScript

import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
import JobDropdown from '~components/job/JobDropdown'
import JobImage from '~components/job/JobImage'
import JobSkillItem from '~components/job/JobSkillItem'
import SearchModal from '~components/search/SearchModal'
import api from '~utils/api'
import { appState } from '~utils/appState'
import type { JobSkillObject, SearchableObject } from '~types'
import './index.scss'
// Props
interface Props {
job?: Job
jobSkills: JobSkillObject
jobAccessory?: JobAccessory
editable: boolean
saveJob: (job?: Job) => void
saveSkill: (skill: JobSkill, position: number) => void
removeSkill: (position: number) => void
saveAccessory: (accessory: JobAccessory) => void
}
const JobSection = (props: Props) => {
const { party } = useSnapshot(appState)
const { t } = useTranslation('common')
const router = useRouter()
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
// Data state
const [job, setJob] = useState<Job>()
const [imageUrl, setImageUrl] = useState('')
const [numSkills, setNumSkills] = useState(4)
const [skills, setSkills] = useState<{ [key: number]: JobSkill | undefined }>(
[]
)
const [accessories, setAccessories] = useState<JobAccessory[]>([])
const [currentAccessory, setCurrentAccessory] = useState<
JobAccessory | undefined
>()
// Refs
const selectRef = React.createRef<HTMLSelectElement>()
// Classes
const skillContainerClasses = classNames({
JobSkills: true,
editable: props.editable,
})
useEffect(() => {
// Set current job based on ID
setJob(props.job)
setSkills({
0: props.jobSkills[0],
1: props.jobSkills[1],
2: props.jobSkills[2],
3: props.jobSkills[3],
})
setCurrentAccessory(props.jobAccessory)
if (selectRef.current && props.job) selectRef.current.value = props.job.id
}, [props])
useEffect(() => {
generateImageUrl()
})
useEffect(() => {
if (job) {
if ((party.job && job.id != party.job.id) || !party.job)
appState.party.job = job
if (job.row === '1') setNumSkills(3)
else setNumSkills(4)
fetchJobAccessories()
}
}, [job])
// Data fetching
async function fetchJobAccessories() {
if (job && job.accessory) {
const response = await api.jobAccessoriesForJob(job.id)
const jobAccessories: JobAccessory[] = response.data
setAccessories(jobAccessories)
}
}
function receiveJob(job?: Job) {
setJob(job)
props.saveJob(job)
}
function handleAccessorySelected(value: string) {
const accessory = accessories.find((accessory) => accessory.id === value)
if (accessory) {
setCurrentAccessory(accessory)
props.saveAccessory(accessory)
}
}
function generateImageUrl() {
let imgSrc = ''
if (job) {
const slug = job?.name.en.replaceAll(' ', '-').toLowerCase()
const gender = party.user && party.user.gender == 1 ? 'b' : 'a'
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png`
}
setImageUrl(imgSrc)
}
const canEditSkill = (skill?: JobSkill) => {
// If there is a job and a skill present in the slot
if (job && job.id !== '-1') {
// If the skill's job is one of the job's main skill
if (skill && skill.job.id === job.id && skill.main) return false
return props.editable
} else return false
}
const skillItem = (index: number, editable: boolean) => {
return (
<JobSkillItem
skill={skills[index]}
position={index}
editable={canEditSkill(skills[index])}
key={`skill-${index}`}
hasJob={job != undefined && job.id != '-1'}
removeJobSkill={props.removeSkill}
/>
)
}
const editableSkillItem = (index: number) => {
return (
<SearchModal
placeholderText={t('search.placeholders.job_skill')}
fromPosition={index}
object="job_skills"
job={job}
send={saveJobSkill}
>
{skillItem(index, true)}
</SearchModal>
)
}
function saveJobSkill(object: SearchableObject, position: number) {
const skill = object as JobSkill
const newSkills = skills
newSkills[position] = skill
setSkills(newSkills)
props.saveSkill(skill, position)
}
const emptyJobLabel = (
<div className="JobName">
<h3>{t('no_job')}</h3>
</div>
)
const filledJobLabel = (
<div className="JobName">
<img
alt={job?.name[locale]}
src={`/images/job-icons/${job?.granblue_id}.png`}
/>
<h3>{job?.name[locale]}</h3>
</div>
)
// Render: JSX components
return (
<section id="Job">
<JobImage
job={party.job}
currentAccessory={currentAccessory}
accessories={accessories}
editable={props.editable}
user={party.user}
onAccessorySelected={handleAccessorySelected}
/>
<div className="JobDetails">
{props.editable ? (
<JobDropdown
currentJob={party.job?.id}
onChange={receiveJob}
ref={selectRef}
/>
) : (
<div className="JobName">
{party.job ? (
<img
alt={party.job.name[locale]}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-icons/${party.job.granblue_id}.png`}
/>
) : (
''
)}
<h3>{party.job ? party.job.name[locale] : t('no_job')}</h3>
</div>
)}
<ul className={skillContainerClasses}>
{[...Array(numSkills)].map((e, i) => (
<li key={`job-${i}`}>
{canEditSkill(skills[i])
? editableSkillItem(i)
: skillItem(i, false)}
</li>
))}
</ul>
</div>
</section>
)
}
export default JobSection