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.
This commit is contained in:
parent
a998413870
commit
35e3ce7b08
5 changed files with 274 additions and 163 deletions
|
|
@ -1,13 +1,7 @@
|
|||
// Core dependencies
|
||||
import React, {
|
||||
PropsWithChildren,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { AxiosResponse } from 'axios'
|
||||
import classNames from 'classnames'
|
||||
|
||||
// UI dependencies
|
||||
|
|
@ -20,14 +14,10 @@ import {
|
|||
import DialogContent from '~components/common/DialogContent'
|
||||
import Button from '~components/common/Button'
|
||||
import SelectWithInput from '~components/common/SelectWithInput'
|
||||
import AwakeningSelect from '~components/mastery/AwakeningSelect'
|
||||
import RingSelect from '~components/mastery/RingSelect'
|
||||
import Switch from '~components/common/Switch'
|
||||
|
||||
// Utilities
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import { retrieveCookies } from '~utils/retrieveCookies'
|
||||
import elementalizeAetherialMastery from '~utils/elementalizeAetherialMastery'
|
||||
|
||||
// Data
|
||||
|
|
@ -36,6 +26,8 @@ const emptyExtendedMastery: ExtendedMastery = {
|
|||
strength: 0,
|
||||
}
|
||||
|
||||
const MAX_AWAKENING_LEVEL = 9
|
||||
|
||||
// Styles and icons
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import './index.scss'
|
||||
|
|
@ -46,6 +38,7 @@ import {
|
|||
ExtendedMastery,
|
||||
GridCharacterObject,
|
||||
} from '~types'
|
||||
import AwakeningSelectWithInput from '~components/mastery/AwakeningSelectWithInput'
|
||||
|
||||
interface Props {
|
||||
gridCharacter: GridCharacter
|
||||
|
|
@ -66,9 +59,6 @@ const CharacterModal = ({
|
|||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Cookies
|
||||
const cookies = retrieveCookies()
|
||||
|
||||
// UI state
|
||||
const [open, setOpen] = useState(false)
|
||||
const [formValid, setFormValid] = useState(false)
|
||||
|
|
@ -103,8 +93,8 @@ const CharacterModal = ({
|
|||
const [earring, setEarring] = useState<ExtendedMastery>(emptyExtendedMastery)
|
||||
|
||||
// Character properties: Awakening
|
||||
const [awakeningType, setAwakeningType] = useState(0)
|
||||
const [awakeningLevel, setAwakeningLevel] = useState(0)
|
||||
const [awakening, setAwakening] = useState<Awakening>()
|
||||
const [awakeningLevel, setAwakeningLevel] = useState(1)
|
||||
|
||||
// Character properties: Transcendence
|
||||
const [transcendenceStep, setTranscendenceStep] = useState(0)
|
||||
|
|
@ -118,7 +108,7 @@ const CharacterModal = ({
|
|||
})
|
||||
}
|
||||
|
||||
setAwakeningType(gridCharacter.awakening.type)
|
||||
setAwakening(gridCharacter.awakening.type)
|
||||
setAwakeningLevel(gridCharacter.awakening.level)
|
||||
setPerpetuity(gridCharacter.perpetuity)
|
||||
}, [gridCharacter])
|
||||
|
|
@ -147,15 +137,16 @@ const CharacterModal = ({
|
|||
modifier: earring.modifier,
|
||||
strength: earring.strength,
|
||||
},
|
||||
awakening: {
|
||||
type: awakeningType,
|
||||
level: awakeningLevel,
|
||||
},
|
||||
transcendence_step: transcendenceStep,
|
||||
perpetuity: perpetuity,
|
||||
},
|
||||
}
|
||||
|
||||
if (awakening) {
|
||||
object.character.awakening_id = awakening.id
|
||||
object.character.awakening_level = awakeningLevel
|
||||
}
|
||||
|
||||
return object
|
||||
}
|
||||
|
||||
|
|
@ -191,8 +182,8 @@ const CharacterModal = ({
|
|||
if (onOpenChange) onOpenChange(false)
|
||||
}
|
||||
|
||||
function receiveAwakeningValues(type: number, level: number) {
|
||||
setAwakeningType(type)
|
||||
function receiveAwakeningValues(id: string, level: number) {
|
||||
setAwakening(gridCharacter.object.awakenings.find((a) => a.id === id))
|
||||
setAwakeningLevel(level)
|
||||
}
|
||||
|
||||
|
|
@ -234,10 +225,16 @@ const CharacterModal = ({
|
|||
return (
|
||||
<section>
|
||||
<h3>{t('modals.characters.subtitles.awakening')}</h3>
|
||||
<AwakeningSelect
|
||||
object="character"
|
||||
type={awakeningType}
|
||||
level={awakeningLevel}
|
||||
<AwakeningSelectWithInput
|
||||
dataSet={gridCharacter.object.awakenings}
|
||||
awakening={gridCharacter.awakening.type}
|
||||
level={gridCharacter.awakening.level}
|
||||
defaultAwakening={
|
||||
gridCharacter.object.awakenings.find(
|
||||
(a) => a.slug === 'character-balanced'
|
||||
)!
|
||||
}
|
||||
maxLevel={MAX_AWAKENING_LEVEL}
|
||||
sendValidity={receiveValidity}
|
||||
sendValues={receiveAwakeningValues}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,97 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
|
||||
import SelectWithInput from '~components/common/SelectWithInput'
|
||||
import { weaponAwakening, characterAwakening } from '~data/awakening'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
object: 'character' | 'weapon'
|
||||
type?: number
|
||||
level?: number
|
||||
onOpenChange?: (open: boolean) => void
|
||||
sendValidity: (isValid: boolean) => void
|
||||
sendValues: (type: number, level: number) => void
|
||||
}
|
||||
|
||||
const AwakeningSelect = (props: Props) => {
|
||||
// Data states
|
||||
const [awakeningType, setAwakeningType] = useState(
|
||||
props.object === 'weapon' ? 0 : 1
|
||||
)
|
||||
const [awakeningLevel, setAwakeningLevel] = useState(1)
|
||||
|
||||
// Data
|
||||
const chooseDataset = () => {
|
||||
let list: ItemSkill[] = []
|
||||
|
||||
switch (props.object) {
|
||||
case 'character':
|
||||
list = characterAwakening
|
||||
break
|
||||
case 'weapon':
|
||||
// WARNING: Clonedeep is masking a deeper error
|
||||
// which is running this method every time this component is rerendered
|
||||
// causing multiple "No awakening" items to be added
|
||||
const awakening = cloneDeep(weaponAwakening)
|
||||
awakening.unshift({
|
||||
id: 0,
|
||||
name: {
|
||||
en: 'No awakening',
|
||||
ja: '覚醒なし',
|
||||
},
|
||||
granblue_id: '',
|
||||
slug: 'no-awakening',
|
||||
minValue: 0,
|
||||
maxValue: 0,
|
||||
fractional: false,
|
||||
})
|
||||
list = awakening
|
||||
break
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
// Set default awakening and level based on object type
|
||||
useEffect(() => {
|
||||
const defaultAwakening = props.object === 'weapon' ? 0 : 1
|
||||
const type = props.type != undefined ? props.type : defaultAwakening
|
||||
|
||||
setAwakeningType(type)
|
||||
setAwakeningLevel(props.level ? props.level : 1)
|
||||
}, [props.object, props.type, props.level])
|
||||
|
||||
// Send validity of form when awakening level changes
|
||||
useEffect(() => {
|
||||
props.sendValidity(awakeningLevel > 0)
|
||||
}, [props.sendValidity, awakeningLevel])
|
||||
|
||||
// Classes
|
||||
function changeOpen(open: boolean) {
|
||||
if (props.onOpenChange) props.onOpenChange(open)
|
||||
}
|
||||
|
||||
function handleValueChange(type: number, level: number) {
|
||||
setAwakeningType(type)
|
||||
setAwakeningLevel(level)
|
||||
props.sendValues(type, level)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="Awakening">
|
||||
<SelectWithInput
|
||||
object={`${props.object}_awakening`}
|
||||
dataSet={chooseDataset()}
|
||||
selectValue={awakeningType}
|
||||
inputValue={awakeningLevel}
|
||||
onOpenChange={changeOpen}
|
||||
sendValidity={props.sendValidity}
|
||||
sendValues={handleValueChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AwakeningSelect
|
||||
|
|
@ -1,4 +1,22 @@
|
|||
.AwakeningSelect .AwakeningSet {
|
||||
.SelectWithItem {
|
||||
.InputSet {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
width: 100%;
|
||||
|
||||
.SelectTrigger {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.Input {
|
||||
flex-grow: 0;
|
||||
text-align: right;
|
||||
width: 13rem;
|
||||
}
|
||||
}
|
||||
|
||||
.errors {
|
||||
color: $error;
|
||||
display: none;
|
||||
|
|
@ -8,30 +26,4 @@
|
|||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.fields {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
width: 100%;
|
||||
|
||||
.SelectTrigger {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.Label {
|
||||
display: none;
|
||||
flex-grow: 0;
|
||||
|
||||
&.Visible {
|
||||
display: block;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.Input {
|
||||
min-width: $unit * 12;
|
||||
width: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
215
components/mastery/AwakeningSelectWithInput/index.tsx
Normal file
215
components/mastery/AwakeningSelectWithInput/index.tsx
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
// Core dependencies
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classNames from 'classnames'
|
||||
|
||||
// UI Dependencies
|
||||
import Input from '~components/common/Input'
|
||||
import Select from '~components/common/Select'
|
||||
import SelectItem from '~components/common/SelectItem'
|
||||
|
||||
// Styles and icons
|
||||
import './index.scss'
|
||||
|
||||
// Types
|
||||
interface Props {
|
||||
dataSet: Awakening[]
|
||||
defaultAwakening: Awakening
|
||||
awakening?: Awakening
|
||||
level?: number
|
||||
maxLevel: number
|
||||
selectDisabled: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
sendValidity: (isValid: boolean) => void
|
||||
sendValues: (type: string, level: number) => void
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
selectDisabled: false,
|
||||
}
|
||||
|
||||
const AwakeningSelectWithInput = ({
|
||||
dataSet,
|
||||
defaultAwakening,
|
||||
awakening,
|
||||
level,
|
||||
maxLevel,
|
||||
selectDisabled,
|
||||
onOpenChange,
|
||||
sendValidity,
|
||||
sendValues,
|
||||
}: Props) => {
|
||||
// Set up translations
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// State: Component
|
||||
const [open, setOpen] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
// State: Data
|
||||
const [currentAwakening, setCurrentAwakening] = useState<Awakening>()
|
||||
const [currentLevel, setCurrentLevel] = useState(1)
|
||||
|
||||
// Refs
|
||||
const inputRef = React.createRef<HTMLInputElement>()
|
||||
|
||||
// Classes
|
||||
const inputClasses = classNames({
|
||||
Bound: true,
|
||||
Hidden: currentAwakening === undefined || currentAwakening.id === '0',
|
||||
})
|
||||
|
||||
const errorClasses = classNames({
|
||||
errors: true,
|
||||
visible: error !== '',
|
||||
})
|
||||
|
||||
// Hooks
|
||||
useEffect(() => {
|
||||
setCurrentAwakening(awakening)
|
||||
setCurrentLevel(level ? level : 1)
|
||||
|
||||
if (awakening) sendValidity(true)
|
||||
}, [])
|
||||
|
||||
// Methods: UI state management
|
||||
function changeOpen() {
|
||||
if (!selectDisabled) {
|
||||
setOpen(!open)
|
||||
if (onOpenChange) onOpenChange(!open)
|
||||
}
|
||||
}
|
||||
|
||||
function onClose() {
|
||||
if (onOpenChange) onOpenChange(false)
|
||||
}
|
||||
|
||||
// Methods: Rendering
|
||||
function generateOptions() {
|
||||
const sortedDataSet = [...dataSet].sort((a, b) => {
|
||||
return a.order - b.order
|
||||
})
|
||||
|
||||
let options: React.ReactNode[] = sortedDataSet.map((awakening, i) => {
|
||||
return generateItem(awakening)
|
||||
})
|
||||
|
||||
if (!dataSet.includes(defaultAwakening))
|
||||
options.unshift(generateItem(defaultAwakening))
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
function generateItem(awakening: Awakening) {
|
||||
return (
|
||||
<SelectItem key={awakening.slug} value={awakening.id}>
|
||||
{awakening.name[locale]}
|
||||
</SelectItem>
|
||||
)
|
||||
}
|
||||
|
||||
// Methods: User input detection
|
||||
function handleSelectChange(id: string) {
|
||||
const input = inputRef.current
|
||||
if (input && !handleInputError(parseFloat(input.value))) return
|
||||
|
||||
setCurrentAwakening(dataSet.find((awakening) => awakening.id === id))
|
||||
sendValues(id, currentLevel)
|
||||
}
|
||||
|
||||
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const input = inputRef.current
|
||||
if (input && !handleInputError(parseFloat(input.value))) return
|
||||
|
||||
setCurrentLevel(parseInt(event.target.value))
|
||||
sendValues(
|
||||
currentAwakening ? currentAwakening.id : '0',
|
||||
parseInt(event.target.value)
|
||||
)
|
||||
}
|
||||
|
||||
// Methods: Handle error
|
||||
|
||||
function handleInputError(value: number) {
|
||||
let error = ''
|
||||
|
||||
if (currentAwakening) {
|
||||
if (value < 1) {
|
||||
error = t(`awakening.errors.value_too_low`, {
|
||||
minValue: 1,
|
||||
})
|
||||
} else if (value > maxLevel) {
|
||||
error = t(`awakening.errors.value_too_high`, {
|
||||
maxValue: maxLevel,
|
||||
})
|
||||
} else if (value % 1 != 0) {
|
||||
error = t(`awakening.errors.value_not_whole`)
|
||||
} else if (!value || value <= 0) {
|
||||
error = t(`awakening.errors.value_empty`)
|
||||
} else {
|
||||
error = ''
|
||||
}
|
||||
}
|
||||
|
||||
setError(error)
|
||||
|
||||
if (error.length > 0) {
|
||||
sendValidity(false)
|
||||
return false
|
||||
} else return true
|
||||
}
|
||||
|
||||
const rangeString = () => {
|
||||
let placeholder = ''
|
||||
|
||||
if (awakening) {
|
||||
const minValue = 1
|
||||
const maxValue = maxLevel
|
||||
placeholder = `${minValue}~${maxValue}`
|
||||
}
|
||||
|
||||
return placeholder
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="SelectWithItem">
|
||||
<div className="InputSet">
|
||||
<Select
|
||||
key="awakening-type"
|
||||
value={`${awakening ? awakening.id : defaultAwakening.id}`}
|
||||
open={open}
|
||||
disabled={selectDisabled}
|
||||
onValueChange={handleSelectChange}
|
||||
onOpenChange={changeOpen}
|
||||
onClose={onClose}
|
||||
triggerClass="modal"
|
||||
overlayVisible={false}
|
||||
>
|
||||
{generateOptions()}
|
||||
</Select>
|
||||
|
||||
<Input
|
||||
value={level ? level : 1}
|
||||
className={inputClasses}
|
||||
type="number"
|
||||
placeholder={rangeString()}
|
||||
min={1}
|
||||
max={maxLevel}
|
||||
step="1"
|
||||
onChange={handleInputChange}
|
||||
visible={awakening ? 'true' : 'false'}
|
||||
ref={inputRef}
|
||||
/>
|
||||
</div>
|
||||
<p className={errorClasses}>{error}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
AwakeningSelectWithInput.defaultProps = defaultProps
|
||||
|
||||
export default AwakeningSelectWithInput
|
||||
|
|
@ -11,14 +11,15 @@ import {
|
|||
DialogTrigger,
|
||||
} from '~components/common/Dialog'
|
||||
import DialogContent from '~components/common/DialogContent'
|
||||
import AwakeningSelectWithInput from '~components/mastery/AwakeningSelectWithInput'
|
||||
import AXSelect from '~components/mastery/AxSelect'
|
||||
import AwakeningSelect from '~components/mastery/AwakeningSelect'
|
||||
import ElementToggle from '~components/ElementToggle'
|
||||
import WeaponKeySelect from '~components/weapon/WeaponKeySelect'
|
||||
import Button from '~components/common/Button'
|
||||
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import { NO_AWAKENING } from '~data/awakening'
|
||||
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import './index.scss'
|
||||
|
|
@ -33,7 +34,7 @@ interface GridWeaponObject {
|
|||
ax_modifier2?: number
|
||||
ax_strength1?: number
|
||||
ax_strength2?: number
|
||||
awakening_type?: number
|
||||
awakening_id?: string
|
||||
awakening_level?: Number
|
||||
}
|
||||
}
|
||||
|
|
@ -70,7 +71,7 @@ const WeaponModal = ({
|
|||
|
||||
const [element, setElement] = useState(-1)
|
||||
|
||||
const [awakeningType, setAwakeningType] = useState(0)
|
||||
const [awakening, setAwakening] = useState<Awakening>()
|
||||
const [awakeningLevel, setAwakeningLevel] = useState(1)
|
||||
|
||||
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
|
||||
|
|
@ -136,9 +137,10 @@ const WeaponModal = ({
|
|||
setFormValid(isValid)
|
||||
}
|
||||
|
||||
function receiveAwakeningValues(type: number, level: number) {
|
||||
setAwakeningType(type)
|
||||
function receiveAwakeningValues(id: string, level: number) {
|
||||
setAwakening(gridWeapon.object.awakenings.find((a) => a.id === id))
|
||||
setAwakeningLevel(level)
|
||||
setFormValid(true)
|
||||
}
|
||||
|
||||
function receiveElementValue(element: string) {
|
||||
|
|
@ -167,8 +169,8 @@ const WeaponModal = ({
|
|||
object.weapon.ax_strength2 = secondaryAxValue
|
||||
}
|
||||
|
||||
if (gridWeapon.object.awakening) {
|
||||
object.weapon.awakening_type = awakeningType
|
||||
if (gridWeapon.object.awakenings) {
|
||||
object.weapon.awakening_id = awakening?.id
|
||||
object.weapon.awakening_level = awakeningLevel
|
||||
}
|
||||
|
||||
|
|
@ -313,10 +315,12 @@ const WeaponModal = ({
|
|||
return (
|
||||
<section>
|
||||
<h3>{t('modals.weapon.subtitles.awakening')}</h3>
|
||||
<AwakeningSelect
|
||||
object="weapon"
|
||||
type={gridWeapon.awakening?.type}
|
||||
<AwakeningSelectWithInput
|
||||
dataSet={gridWeapon.object.awakenings}
|
||||
awakening={gridWeapon.awakening?.type}
|
||||
level={gridWeapon.awakening?.level}
|
||||
defaultAwakening={NO_AWAKENING}
|
||||
maxLevel={gridWeapon.object.max_awakening_level}
|
||||
onOpenChange={receiveAwakeningOpen}
|
||||
sendValidity={receiveValidity}
|
||||
sendValues={receiveAwakeningValues}
|
||||
|
|
@ -326,7 +330,7 @@ const WeaponModal = ({
|
|||
}
|
||||
|
||||
function handleOpenChange(open: boolean) {
|
||||
if (gridWeapon.object.ax || gridWeapon.object.awakening) {
|
||||
if (gridWeapon.object.ax || gridWeapon.object.awakenings) {
|
||||
setFormValid(false)
|
||||
} else {
|
||||
setFormValid(true)
|
||||
|
|
@ -387,7 +391,7 @@ const WeaponModal = ({
|
|||
{gridWeapon.object.element == 0 ? elementSelect() : ''}
|
||||
{[2, 3, 17, 24].includes(gridWeapon.object.series) ? keySelect() : ''}
|
||||
{gridWeapon.object.ax ? axSelect() : ''}
|
||||
{gridWeapon.awakening ? awakeningSelect() : ''}
|
||||
{gridWeapon.object.awakenings ? awakeningSelect() : ''}
|
||||
</div>
|
||||
<div className="DialogFooter" ref={footerRef}>
|
||||
<Button
|
||||
|
|
|
|||
Loading…
Reference in a new issue