* 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
144 lines
4 KiB
TypeScript
144 lines
4 KiB
TypeScript
import React, { useEffect, useState } from 'react'
|
|
import { useRouter } from 'next/router'
|
|
import { useSnapshot } from 'valtio'
|
|
import { useTranslation } from 'next-i18next'
|
|
|
|
import Select from '~components/common/Select'
|
|
import SelectItem from '~components/common/SelectItem'
|
|
import SelectGroup from '~components/common/SelectGroup'
|
|
|
|
import { appState } from '~utils/appState'
|
|
import { jobGroups } from '~data/jobGroups'
|
|
|
|
import './index.scss'
|
|
|
|
// Props
|
|
interface Props {
|
|
currentJob?: string
|
|
onChange?: (job?: Job) => void
|
|
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => void
|
|
}
|
|
|
|
type GroupedJob = { [key: string]: Job[] }
|
|
|
|
const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
|
|
function useFieldSet(props, ref) {
|
|
// Set up router for locale
|
|
const router = useRouter()
|
|
const locale = router.locale || 'en'
|
|
|
|
// Set up translation
|
|
const { t } = useTranslation('common')
|
|
|
|
// Create snapshot of app state
|
|
const { party } = useSnapshot(appState)
|
|
|
|
// Set up local states for storing jobs
|
|
const [open, setOpen] = useState(false)
|
|
const [currentJob, setCurrentJob] = useState<Job>()
|
|
const [jobs, setJobs] = useState<Job[]>()
|
|
const [sortedJobs, setSortedJobs] = useState<GroupedJob>()
|
|
|
|
// Set current job from state on mount
|
|
useEffect(() => {
|
|
if (party.job?.id !== '-1') {
|
|
setCurrentJob(party.job)
|
|
}
|
|
}, [])
|
|
|
|
// Organize jobs into groups on mount
|
|
useEffect(() => {
|
|
const jobGroups = appState.jobs
|
|
.map((job) => job.row)
|
|
.filter((value, index, self) => self.indexOf(value) === index)
|
|
let groupedJobs: GroupedJob = {}
|
|
|
|
jobGroups.forEach((group) => {
|
|
groupedJobs[group] = appState.jobs.filter((job) => job.row === group)
|
|
})
|
|
|
|
setJobs(appState.jobs)
|
|
setSortedJobs(groupedJobs)
|
|
}, [appState])
|
|
|
|
// Set current job on mount
|
|
useEffect(() => {
|
|
if (jobs && props.currentJob) {
|
|
const job = appState.jobs.find((job) => job.id === props.currentJob)
|
|
setCurrentJob(job)
|
|
}
|
|
}, [appState, props.currentJob])
|
|
|
|
function openJobSelect() {
|
|
setOpen(!open)
|
|
}
|
|
|
|
// Enable changing select value
|
|
function handleChange(value: string) {
|
|
if (jobs) {
|
|
const job = jobs.find((job) => job.id === value)
|
|
if (props.onChange) props.onChange(job)
|
|
setCurrentJob(job)
|
|
}
|
|
}
|
|
|
|
// Render JSX for each job option, sorted into optgroups
|
|
function renderJobGroup(group: string) {
|
|
const options =
|
|
sortedJobs &&
|
|
sortedJobs[group].length > 0 &&
|
|
sortedJobs[group]
|
|
.sort((a, b) => a.order - b.order)
|
|
.map((item, i) => {
|
|
return (
|
|
<SelectItem
|
|
key={i}
|
|
value={item.id}
|
|
altText={item.name[locale]}
|
|
iconSrc={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-icons/${item.granblue_id}.png`}
|
|
>
|
|
{item.name[locale]}
|
|
</SelectItem>
|
|
)
|
|
})
|
|
|
|
const groupName = jobGroups.find((g) => g.slug === group)?.name[locale]
|
|
|
|
return (
|
|
<SelectGroup key={group} label={groupName} separator={false}>
|
|
{options}
|
|
</SelectGroup>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Select
|
|
value={currentJob ? currentJob.id : 'no-job'}
|
|
altText={currentJob ? currentJob.name[locale] : ''}
|
|
iconSrc={
|
|
currentJob
|
|
? `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-icons/${currentJob.granblue_id}.png`
|
|
: ''
|
|
}
|
|
open={open}
|
|
onClick={openJobSelect}
|
|
onOpenChange={() => setOpen(!open)}
|
|
onValueChange={handleChange}
|
|
className="Job"
|
|
triggerClass="Job"
|
|
overlayVisible={false}
|
|
>
|
|
<SelectItem key={-1} value="no-job">
|
|
{t('no_job')}
|
|
</SelectItem>
|
|
{sortedJobs
|
|
? Object.keys(sortedJobs)
|
|
.sort((a, b) => ('' + a).localeCompare(b))
|
|
.map((x) => renderJobGroup(x))
|
|
: ''}
|
|
</Select>
|
|
)
|
|
}
|
|
)
|
|
|
|
export default JobDropdown
|