Update WeaponModal to incorporate latest changes

* Adds unsaved changes alert
* Updates to use refactored WeaponKeySelect
* Moves api code to parent via a updateWeapon prop
* Updates to use DialogHeader and DialogFooter
* Makes rendering functions into constants
This commit is contained in:
Justin Edmund 2023-07-03 19:07:25 -07:00
parent c60b9887e3
commit 932dfe231f

View file

@ -1,15 +1,14 @@
import React, { PropsWithChildren, useEffect, useState } from 'react' import React, { PropsWithChildren, useEffect, useState } from 'react'
import { getCookie } from 'cookies-next' import { getCookie } from 'cookies-next'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' import { Trans, useTranslation } from 'next-i18next'
import { AxiosResponse } from 'axios' import { isEqual } from 'lodash'
import { import { GridWeaponObject } from '~types'
Dialog, import Alert from '~components/common/Alert'
DialogClose, import { Dialog, DialogTrigger } from '~components/common/Dialog'
DialogTitle, import DialogHeader from '~components/common/DialogHeader'
DialogTrigger, import DialogFooter from '~components/common/DialogFooter'
} from '~components/common/Dialog'
import DialogContent from '~components/common/DialogContent' import DialogContent from '~components/common/DialogContent'
import AwakeningSelectWithInput from '~components/mastery/AwakeningSelectWithInput' import AwakeningSelectWithInput from '~components/mastery/AwakeningSelectWithInput'
import AXSelect from '~components/mastery/AxSelect' import AXSelect from '~components/mastery/AxSelect'
@ -17,17 +16,15 @@ import ElementToggle from '~components/ElementToggle'
import WeaponKeySelect from '~components/weapon/WeaponKeySelect' import WeaponKeySelect from '~components/weapon/WeaponKeySelect'
import Button from '~components/common/Button' import Button from '~components/common/Button'
import api from '~utils/api'
import { appState } from '~utils/appState'
import { NO_AWAKENING } from '~data/awakening' import { NO_AWAKENING } from '~data/awakening'
import CrossIcon from '~public/icons/Cross.svg'
import styles from './index.module.scss' import styles from './index.module.scss'
interface Props { interface Props {
gridWeapon: GridWeapon gridWeapon: GridWeapon
open: boolean open: boolean
onOpenChange: (open: boolean) => void onOpenChange: (open: boolean) => void
updateWeapon: (object: GridWeaponObject) => Promise<any>
} }
const WeaponModal = ({ const WeaponModal = ({
@ -35,6 +32,7 @@ const WeaponModal = ({
open: modalOpen, open: modalOpen,
children, children,
onOpenChange, onOpenChange,
updateWeapon,
}: PropsWithChildren<Props>) => { }: PropsWithChildren<Props>) => {
const router = useRouter() const router = useRouter()
const locale = const locale =
@ -50,27 +48,11 @@ const WeaponModal = ({
? { Authorization: `Bearer ${accountData.token}` } ? { Authorization: `Bearer ${accountData.token}` }
: {} : {}
// State // State: Component
const [open, setOpen] = useState(false) const [alertOpen, setAlertOpen] = useState(false)
const [formValid, setFormValid] = useState(false) const [formValid, setFormValid] = useState(false)
const [element, setElement] = useState(-1) // State: Selects
const [awakening, setAwakening] = useState<Awakening>()
const [awakeningLevel, setAwakeningLevel] = useState(1)
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1)
const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
const [weaponKey1, setWeaponKey1] = useState<WeaponKey | undefined>()
const [weaponKey2, setWeaponKey2] = useState<WeaponKey | undefined>()
const [weaponKey3, setWeaponKey3] = useState<WeaponKey | undefined>()
const [weaponKey1Id, setWeaponKey1Id] = useState('')
const [weaponKey2Id, setWeaponKey2Id] = useState('')
const [weaponKey3Id, setWeaponKey3Id] = useState('')
const [weaponKey1Open, setWeaponKey1Open] = useState(false) const [weaponKey1Open, setWeaponKey1Open] = useState(false)
const [weaponKey2Open, setWeaponKey2Open] = useState(false) const [weaponKey2Open, setWeaponKey2Open] = useState(false)
const [weaponKey3Open, setWeaponKey3Open] = useState(false) const [weaponKey3Open, setWeaponKey3Open] = useState(false)
@ -79,16 +61,26 @@ const WeaponModal = ({
const [ax2Open, setAx2Open] = useState(false) const [ax2Open, setAx2Open] = useState(false)
const [awakeningOpen, setAwakeningOpen] = useState(false) const [awakeningOpen, setAwakeningOpen] = useState(false)
// State: Data
const [element, setElement] = useState<number>(0)
const [awakening, setAwakening] = useState<Awakening>()
const [awakeningLevel, setAwakeningLevel] = useState(1)
const [primaryAxModifier, setPrimaryAxModifier] = useState(-1)
const [secondaryAxModifier, setSecondaryAxModifier] = useState(-1)
const [primaryAxValue, setPrimaryAxValue] = useState(0.0)
const [secondaryAxValue, setSecondaryAxValue] = useState(0.0)
const [weaponKey1, setWeaponKey1] = useState<WeaponKey | undefined>()
const [weaponKey2, setWeaponKey2] = useState<WeaponKey | undefined>()
const [weaponKey3, setWeaponKey3] = useState<WeaponKey | undefined>()
// Refs // Refs
const headerRef = React.createRef<HTMLDivElement>() const headerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>() const footerRef = React.createRef<HTMLDivElement>()
// Hooks // Hooks
useEffect(() => {
setOpen(modalOpen)
handleOpenChange(modalOpen)
}, [modalOpen])
// Set up modal data state when the gridWeapon changes
useEffect(() => { useEffect(() => {
setElement(gridWeapon.element) setElement(gridWeapon.element)
@ -105,6 +97,14 @@ const WeaponModal = ({
} }
}, [gridWeapon]) }, [gridWeapon])
// Methods: Data retrieval
// Receive values from ElementToggle
function receiveElementValue(elementId: number) {
setElement(elementId)
}
// Receive values from AXSelect
function receiveAxValues( function receiveAxValues(
primaryAxModifier: number, primaryAxModifier: number,
primaryAxValue: number, primaryAxValue: number,
@ -118,34 +118,40 @@ const WeaponModal = ({
setSecondaryAxValue(secondaryAxValue) setSecondaryAxValue(secondaryAxValue)
} }
function receiveValidity(isValid: boolean) { // Receive values from AwakeningSelectWithInput
setFormValid(isValid)
}
function receiveAwakeningValues(id: string, level: number) { function receiveAwakeningValues(id: string, level: number) {
setAwakening(gridWeapon.object.awakenings.find((a) => a.id === id)) setAwakening(gridWeapon.object.awakenings.find((a) => a.id === id))
setAwakeningLevel(level) setAwakeningLevel(level)
setFormValid(true) setFormValid(true)
} }
function receiveElementValue(element: string) { // Receive values from WeaponKeySelect
setElement(parseInt(element)) function receiveWeaponKey(value: WeaponKey, slot: number) {
if (slot === 0) setWeaponKey1(value)
if (slot === 1) setWeaponKey2(value)
if (slot === 2) setWeaponKey3(value)
} }
// Receive form validity from child components
function receiveValidity(isValid: boolean) {
setFormValid(isValid)
}
// Methods: Data submission
function prepareObject() { function prepareObject() {
let object: GridWeaponObject = { weapon: {} } let object: GridWeaponObject = { weapon: {} }
if (gridWeapon.object.element == 0) object.weapon.element = element if (gridWeapon.object.element == 0) object.weapon.element = element
if ([2, 3, 17, 24].includes(gridWeapon.object.series) && weaponKey1Id) { if ([2, 3, 17, 24].includes(gridWeapon.object.series) && weaponKey1) {
object.weapon.weapon_key1_id = weaponKey1Id object.weapon.weapon_key1_id = weaponKey1.id
} }
if ([2, 3, 17].includes(gridWeapon.object.series) && weaponKey2Id) if ([2, 3, 17].includes(gridWeapon.object.series) && weaponKey2)
object.weapon.weapon_key2_id = weaponKey2Id object.weapon.weapon_key2_id = weaponKey2.id
if (gridWeapon.object.series == 17 && weaponKey3Id) if (gridWeapon.object.series == 17 && weaponKey3)
object.weapon.weapon_key3_id = weaponKey3Id object.weapon.weapon_key3_id = weaponKey3.id
if (gridWeapon.object.ax && gridWeapon.object.ax_type > 0) { if (gridWeapon.object.ax && gridWeapon.object.ax_type > 0) {
object.weapon.ax_modifier1 = primaryAxModifier object.weapon.ax_modifier1 = primaryAxModifier
@ -162,45 +168,20 @@ const WeaponModal = ({
return object return object
} }
async function updateWeapon() { async function handleUpdateWeapon() {
const updateObject = prepareObject() await updateWeapon(prepareObject())
return await api.endpoints.grid_weapons
.update(gridWeapon.id, updateObject, headers)
.then((response) => processResult(response))
.catch((error) => processError(error))
}
function processResult(response: AxiosResponse) {
const gridWeapon: GridWeapon = response.data
if (gridWeapon.mainhand) appState.grid.weapons.mainWeapon = gridWeapon
else appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon
if (onOpenChange) onOpenChange(false) if (onOpenChange) onOpenChange(false)
setOpen(false)
} }
function processError(error: any) { // Methods: Event handlers
console.error(error) const anySelectOpen =
} weaponKey1Open ||
weaponKey2Open ||
function receiveWeaponKey(value: string, slot: number) { weaponKey3Open ||
if (slot === 0) setWeaponKey1Id(value) weaponKey4Open ||
if (slot === 1) setWeaponKey2Id(value) ax1Open ||
if (slot === 2) setWeaponKey3Id(value) ax2Open ||
} awakeningOpen
const elementSelect = () => {
return (
<section>
<h3>{t('modals.weapon.subtitles.element')}</h3>
<ElementToggle
currentElement={element}
sendValue={receiveElementValue}
/>
</section>
)
}
function openSelect(index: 1 | 2 | 3 | 4) { function openSelect(index: 1 | 2 | 3 | 4) {
setWeaponKey1Open(index === 1 ? !weaponKey1Open : false) setWeaponKey1Open(index === 1 ? !weaponKey1Open : false)
@ -218,14 +199,158 @@ const WeaponModal = ({
setAwakeningOpen(isOpen) setAwakeningOpen(isOpen)
} }
const keySelect = () => { function close() {
// Reset values
setElement(gridWeapon.element)
setWeaponKey1(
gridWeapon.weapon_keys && gridWeapon.weapon_keys[0]
? gridWeapon.weapon_keys[0]
: undefined
)
setWeaponKey2(
gridWeapon.weapon_keys && gridWeapon.weapon_keys[1]
? gridWeapon.weapon_keys[1]
: undefined
)
setWeaponKey3(
gridWeapon.weapon_keys && gridWeapon.weapon_keys[2]
? gridWeapon.weapon_keys[2]
: undefined
)
setAwakening(gridWeapon.awakening?.type)
setAwakeningLevel(gridWeapon.awakening?.level || 1)
setAlertOpen(false)
onOpenChange(false)
}
function handleOpenChange(open: boolean) {
console.log(`Modal is currently open? ${open}`)
if (modalOpen && hasBeenModified()) {
setAlertOpen(true)
} else {
if (gridWeapon.object.ax || gridWeapon.object.awakenings) {
setFormValid(false)
} else {
setFormValid(true)
}
onOpenChange(open)
}
}
function onEscapeKeyDown(event: KeyboardEvent) {
if (anySelectOpen) {
return event.preventDefault()
} else if (hasBeenModified()) {
setAlertOpen(true)
} else {
close()
}
}
// Methods: Modification checking
function hasBeenModified() {
return ( return (
elementChanged() ||
weaponKeysChanged() ||
axChanged() ||
awakeningChanged()
)
}
function elementChanged() {
if (gridWeapon.object.element === 0 && gridWeapon.element) return false
return element !== gridWeapon.element
}
function weaponKeyChanged(index: number) {
// Get the correct key ID from the given index and
// reset it to an empty string if it's 'no-key'
let weaponKey =
index === 0 ? weaponKey1 : index === 1 ? weaponKey2 : weaponKey3
if (weaponKey && weaponKey.id === 'no-key') weaponKey = undefined
// If the key is empty and the gridWeapon has no keys, nothing has changed
if (weaponKey === undefined && !gridWeapon.weapon_keys) return false
// If the key is not empty but the gridWeapon has no keys, the key has changed
if (
weaponKey !== undefined &&
gridWeapon.weapon_keys &&
gridWeapon.weapon_keys.length === 0
)
return true
// If gridWeapon has a key at the current index, but it doesn't match the key ID,
// then the key has changed
const weaponKeyChanged =
weaponKey &&
gridWeapon.weapon_keys &&
gridWeapon.weapon_keys[index] &&
weaponKey.id !== gridWeapon.weapon_keys[index].id
return weaponKeyChanged
}
function weaponKeysChanged() {
if (!gridWeapon.weapon_keys) return false
const weaponKey1Changed = weaponKeyChanged(0)
const weaponKey2Changed = weaponKeyChanged(1)
const weaponKey3Changed = weaponKeyChanged(2)
return weaponKey1Changed || weaponKey2Changed || weaponKey3Changed
}
function axChanged() {
if (!gridWeapon.ax) return false
const ax1Changed =
gridWeapon.ax[0].modifier !== primaryAxModifier ||
gridWeapon.ax[0].strength !== primaryAxValue
// console.log(`Has ax 1 changed? ${ax1Changed}`)
const ax2Changed =
gridWeapon.ax[1].modifier !== secondaryAxModifier ||
gridWeapon.ax[1].strength !== secondaryAxValue
// console.log(`Has ax 2 changed? ${ax2Changed}`)
return ax1Changed || ax2Changed
}
function awakeningChanged() {
if (!gridWeapon.awakening) return false
// Check if the awakening in local state is different from the one on the current GridCharacter
const awakeningChanged =
!isEqual(gridWeapon.awakening.type, awakening) ||
gridWeapon.awakening.level !== awakeningLevel
// console.log(`Has awakening changed? ${awakeningChanged}`)
// Return true if the awakening has been modified and is not empty
return awakeningChanged
}
// Methods: Rendering
const elementSelect = (
<section>
<h3>{t('modals.weapon.subtitles.element')}</h3>
<ElementToggle
currentElement={gridWeapon.element}
sendValue={receiveElementValue}
/>
</section>
)
const keySelect = (
<section> <section>
<h3>{t('modals.weapon.subtitles.weapon_keys')}</h3> <h3>{t('modals.weapon.subtitles.weapon_keys')}</h3>
{[2, 3, 17, 22].includes(gridWeapon.object.series) ? ( {[2, 3, 17, 22].includes(gridWeapon.object.series) ? (
<WeaponKeySelect <WeaponKeySelect
open={weaponKey1Open} open={weaponKey1Open}
currentValue={weaponKey1 != null ? weaponKey1 : undefined} weaponKey={weaponKey1}
series={gridWeapon.object.series} series={gridWeapon.object.series}
slot={0} slot={0}
onOpenChange={() => openSelect(1)} onOpenChange={() => openSelect(1)}
@ -239,7 +364,7 @@ const WeaponModal = ({
{[2, 3, 17].includes(gridWeapon.object.series) ? ( {[2, 3, 17].includes(gridWeapon.object.series) ? (
<WeaponKeySelect <WeaponKeySelect
open={weaponKey2Open} open={weaponKey2Open}
currentValue={weaponKey2 != null ? weaponKey2 : undefined} weaponKey={weaponKey2}
series={gridWeapon.object.series} series={gridWeapon.object.series}
slot={1} slot={1}
onOpenChange={() => openSelect(2)} onOpenChange={() => openSelect(2)}
@ -253,7 +378,7 @@ const WeaponModal = ({
{gridWeapon.object.series == 17 ? ( {gridWeapon.object.series == 17 ? (
<WeaponKeySelect <WeaponKeySelect
open={weaponKey3Open} open={weaponKey3Open}
currentValue={weaponKey3 != null ? weaponKey3 : undefined} weaponKey={weaponKey3}
series={gridWeapon.object.series} series={gridWeapon.object.series}
slot={2} slot={2}
onOpenChange={() => openSelect(3)} onOpenChange={() => openSelect(3)}
@ -267,7 +392,7 @@ const WeaponModal = ({
{gridWeapon.object.series == 24 && gridWeapon.object.uncap.ulb ? ( {gridWeapon.object.series == 24 && gridWeapon.object.uncap.ulb ? (
<WeaponKeySelect <WeaponKeySelect
open={weaponKey4Open} open={weaponKey4Open}
currentValue={weaponKey1 != null ? weaponKey1 : undefined} weaponKey={weaponKey1}
series={gridWeapon.object.series} series={gridWeapon.object.series}
slot={0} slot={0}
onOpenChange={() => openSelect(4)} onOpenChange={() => openSelect(4)}
@ -279,10 +404,8 @@ const WeaponModal = ({
)} )}
</section> </section>
) )
}
const axSelect = () => { const axSelect = (
return (
<section> <section>
<h3>{t('modals.weapon.subtitles.ax_skills')}</h3> <h3>{t('modals.weapon.subtitles.ax_skills')}</h3>
<AXSelect <AXSelect
@ -294,10 +417,8 @@ const WeaponModal = ({
/> />
</section> </section>
) )
}
const awakeningSelect = () => { const awakeningSelect = (
return (
<section> <section>
<h3>{t('modals.weapon.subtitles.awakening')}</h3> <h3>{t('modals.weapon.subtitles.awakening')}</h3>
<AwakeningSelectWithInput <AwakeningSelectWithInput
@ -312,37 +433,33 @@ const WeaponModal = ({
/> />
</section> </section>
) )
}
function handleOpenChange(open: boolean) { const confirmationAlert = (
if (gridWeapon.object.ax || gridWeapon.object.awakenings) { <Alert
setFormValid(false) message={
} else { <span>
setFormValid(true) <Trans i18nKey="alerts.unsaved_changes.object">
} You will lose all changes to{' '}
setOpen(open) <strong>{{ objectName: gridWeapon.object.name[locale] }}</strong> if
onOpenChange(open) you continue.
} <br />
<br />
const anySelectOpen = Are you sure you want to continue without saving?
weaponKey1Open || </Trans>
weaponKey2Open || </span>
weaponKey3Open ||
weaponKey4Open ||
ax1Open ||
ax2Open ||
awakeningOpen
function onEscapeKeyDown(event: KeyboardEvent) {
if (anySelectOpen) {
return event.preventDefault()
} else {
setOpen(false)
}
} }
open={alertOpen}
primaryActionText="Close"
primaryAction={close}
cancelActionText="Nevermind"
cancelAction={() => setAlertOpen(false)}
/>
)
return ( return (
<Dialog open={open} onOpenChange={handleOpenChange}> <>
{confirmationAlert}
<Dialog open={modalOpen} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger> <DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent <DialogContent
className="Weapon" className="Weapon"
@ -351,45 +468,36 @@ const WeaponModal = ({
onOpenAutoFocus={(event) => event.preventDefault()} onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={onEscapeKeyDown} onEscapeKeyDown={onEscapeKeyDown}
> >
<div className="DialogHeader Short" ref={headerRef}> <DialogHeader
<img ref={headerRef}
alt={gridWeapon.object.name[locale]} title={gridWeapon.object.name[locale]}
className="DialogImage" subtitle={t('modals.characters.title')}
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-square/${gridWeapon.object.granblue_id}.jpg`} image={{
src: `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-square/${gridWeapon.object.granblue_id}.jpg`,
alt: gridWeapon.object.name[locale],
}}
/> />
<div className="DialogTop"> <section className={styles.mods}>
<DialogTitle className="SubTitle"> {gridWeapon.object.element == 0 && elementSelect}
{t('modals.weapon.title')} {[2, 3, 17, 24].includes(gridWeapon.object.series) && keySelect}
</DialogTitle> {gridWeapon.object.ax && axSelect}
<DialogTitle className="DialogTitle"> {gridWeapon.object.awakenings && awakeningSelect}
{gridWeapon.object.name[locale]} </section>
</DialogTitle> <DialogFooter
</div> ref={footerRef}
<DialogClose className="DialogClose" asChild> rightElements={[
<span>
<CrossIcon />
</span>
</DialogClose>
</div>
<div className="mods">
{gridWeapon.object.element == 0 ? elementSelect() : ''}
{[2, 3, 17, 24].includes(gridWeapon.object.series) ? keySelect() : ''}
{gridWeapon.object.ax ? axSelect() : ''}
{gridWeapon.object.awakenings ? awakeningSelect() : ''}
</div>
<div className="DialogFooter" ref={footerRef}>
<div className="actions">
<Button <Button
bound={true} bound={true}
onClick={updateWeapon} onClick={handleUpdateWeapon}
key="confirm"
disabled={!formValid} disabled={!formValid}
text={t('modals.weapon.buttons.confirm')} text={t('modals.weapon.buttons.confirm')}
/>,
]}
/> />
</div>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</>
) )
} }