From 0ebd1a6c664402516ba87ed1a167f5795f77c146 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Mon, 19 Jun 2023 00:46:03 -0700 Subject: [PATCH 1/2] Update awakening (#315) * 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 --- .../character/CharacterHovercard/index.tsx | 27 +-- components/character/CharacterModal/index.tsx | 51 ++--- components/mastery/AwakeningSelect/index.tsx | 97 -------- .../index.scss | 46 ++-- .../AwakeningSelectWithInput/index.tsx | 215 ++++++++++++++++++ components/weapon/WeaponHovercard/index.tsx | 12 +- components/weapon/WeaponModal/index.tsx | 28 ++- components/weapon/WeaponUnit/index.tsx | 18 +- data/awakening.tsx | 103 +-------- types/Awakening.d.ts | 11 + types/Character.d.ts | 1 + types/GridCharacter.d.ts | 2 +- types/GridWeapon.d.ts | 2 +- types/Weapon.d.ts | 3 +- types/index.d.ts | 6 +- 15 files changed, 316 insertions(+), 306 deletions(-) delete mode 100644 components/mastery/AwakeningSelect/index.tsx rename components/mastery/{AwakeningSelect => AwakeningSelectWithInput}/index.scss (54%) create mode 100644 components/mastery/AwakeningSelectWithInput/index.tsx create mode 100644 types/Awakening.d.ts diff --git a/components/character/CharacterHovercard/index.tsx b/components/character/CharacterHovercard/index.tsx index 47d066ff..b65d23a2 100644 --- a/components/character/CharacterHovercard/index.tsx +++ b/components/character/CharacterHovercard/index.tsx @@ -16,7 +16,6 @@ import { aetherialMastery, permanentMastery, } from '~data/overMastery' -import { characterAwakening } from '~data/awakening' import { ExtendedMastery } from '~types' import './index.scss' @@ -27,13 +26,6 @@ interface Props { onTriggerClick: () => void } -interface KeyNames { - [key: string]: { - en: string - jp: string - } -} - const CharacterHovercard = (props: Props) => { const router = useRouter() const { t } = useTranslation('common') @@ -181,27 +173,20 @@ const CharacterHovercard = (props: Props) => { const awakeningSection = () => { const gridAwakening = props.gridCharacter.awakening - const awakening = characterAwakening.find( - (awakening) => awakening.id === gridAwakening?.type - ) - if (gridAwakening && awakening) { + if (gridAwakening) { return (
{t('modals.characters.subtitles.awakening')}
- {gridAwakening.type > 1 ? ( - {awakening.name[locale]} - ) : ( - '' - )} + {gridAwakening.type.name[locale]} - {`${awakening.name[locale]}`}  + {`${gridAwakening.type.name[locale]}`}  {`Lv${gridAwakening.level}`}
diff --git a/components/character/CharacterModal/index.tsx b/components/character/CharacterModal/index.tsx index c23fc881..29568c76 100644 --- a/components/character/CharacterModal/index.tsx +++ b/components/character/CharacterModal/index.tsx @@ -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(emptyExtendedMastery) // Character properties: Awakening - const [awakeningType, setAwakeningType] = useState(0) - const [awakeningLevel, setAwakeningLevel] = useState(0) + const [awakening, setAwakening] = useState() + 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 (

{t('modals.characters.subtitles.awakening')}

- a.slug === 'character-balanced' + )! + } + maxLevel={MAX_AWAKENING_LEVEL} sendValidity={receiveValidity} sendValues={receiveAwakeningValues} /> diff --git a/components/mastery/AwakeningSelect/index.tsx b/components/mastery/AwakeningSelect/index.tsx deleted file mode 100644 index 3271403e..00000000 --- a/components/mastery/AwakeningSelect/index.tsx +++ /dev/null @@ -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 ( -
- -
- ) -} - -export default AwakeningSelect diff --git a/components/mastery/AwakeningSelect/index.scss b/components/mastery/AwakeningSelectWithInput/index.scss similarity index 54% rename from components/mastery/AwakeningSelect/index.scss rename to components/mastery/AwakeningSelectWithInput/index.scss index 59799bff..659676be 100644 --- a/components/mastery/AwakeningSelect/index.scss +++ b/components/mastery/AwakeningSelectWithInput/index.scss @@ -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; - } - } - } } diff --git a/components/mastery/AwakeningSelectWithInput/index.tsx b/components/mastery/AwakeningSelectWithInput/index.tsx new file mode 100644 index 00000000..80bf5ed9 --- /dev/null +++ b/components/mastery/AwakeningSelectWithInput/index.tsx @@ -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() + const [currentLevel, setCurrentLevel] = useState(1) + + // Refs + const inputRef = React.createRef() + + // 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 ( + + {awakening.name[locale]} + + ) + } + + // 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) { + 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 ( +
+
+ + + +
+

{error}

+
+ ) +} + +AwakeningSelectWithInput.defaultProps = defaultProps + +export default AwakeningSelectWithInput diff --git a/components/weapon/WeaponHovercard/index.tsx b/components/weapon/WeaponHovercard/index.tsx index c0e3f48b..1e9132c1 100644 --- a/components/weapon/WeaponHovercard/index.tsx +++ b/components/weapon/WeaponHovercard/index.tsx @@ -12,7 +12,6 @@ import WeaponLabelIcon from '~components/weapon/WeaponLabelIcon' import UncapIndicator from '~components/uncap/UncapIndicator' import ax from '~data/ax' -import { weaponAwakening } from '~data/awakening' import './index.scss' @@ -146,11 +145,8 @@ const WeaponHovercard = (props: Props) => { const awakeningSection = () => { const gridAwakening = props.gridWeapon.awakening - const awakening = weaponAwakening.find( - (awakening) => awakening.id === gridAwakening?.type - ) - if (gridAwakening && awakening) { + if (gridAwakening) { return (
@@ -158,11 +154,11 @@ const WeaponHovercard = (props: Props) => {
{awakening.name[locale]} - {`${awakening.name[locale]}`}  + {`${gridAwakening.type.name[locale]}`}  {`Lv${gridAwakening.level}`}
diff --git a/components/weapon/WeaponModal/index.tsx b/components/weapon/WeaponModal/index.tsx index 157529be..477b78e9 100644 --- a/components/weapon/WeaponModal/index.tsx +++ b/components/weapon/WeaponModal/index.tsx @@ -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() 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 (

{t('modals.weapon.subtitles.awakening')}

-