hensei-web/components/weapon/WeaponModal/index.tsx
Justin Edmund 9e6c9a2108
Implement party visibility (#369)
Parties can now be set to be private or unlisted. Private parties cannot
be shared with anyone while Unlisted parties can be seen by those with
the link.

We implemented a dialog to change visibility, notices to let users know
if a party isn't public, and icons on the GridRep so users can see at a
glance which of their parties has different visibility on their profile.

![CleanShot 2023-08-25 at 15 50
10@2x](https://github.com/jedmund/hensei-web/assets/383021/488b7fe2-497a-48f3-982a-d603c0a34539)

![CleanShot 2023-08-25 at 15 49
45@2x](https://github.com/jedmund/hensei-web/assets/383021/675523f6-d158-4019-8c1a-cf87b48501f9)

![CleanShot 2023-08-25 at 15 50
49@2x](https://github.com/jedmund/hensei-web/assets/383021/419a3b06-f083-4c9e-b4fb-ea70669513fd)
2023-08-25 15:51:28 -07:00

510 lines
15 KiB
TypeScript

import React, { PropsWithChildren, useEffect, useState } from 'react'
import { getCookie } from 'cookies-next'
import { useRouter } from 'next/router'
import { Trans, useTranslation } from 'next-i18next'
import { isEqual } from 'lodash'
import { GridWeaponObject } from '~types'
import Alert from '~components/common/Alert'
import { Dialog, DialogTrigger } from '~components/common/Dialog'
import DialogHeader from '~components/common/DialogHeader'
import DialogFooter from '~components/common/DialogFooter'
import DialogContent from '~components/common/DialogContent'
import AwakeningSelectWithInput from '~components/mastery/AwakeningSelectWithInput'
import AXSelect from '~components/mastery/AxSelect'
import ElementToggle from '~components/ElementToggle'
import WeaponKeySelect from '~components/weapon/WeaponKeySelect'
import Button from '~components/common/Button'
import { NO_AWAKENING } from '~data/awakening'
import styles from './index.module.scss'
interface Props {
gridWeapon: GridWeapon
open: boolean
onOpenChange: (open: boolean) => void
updateWeapon: (object: GridWeaponObject) => Promise<any>
}
const WeaponModal = ({
gridWeapon,
open: modalOpen,
children,
onOpenChange,
updateWeapon,
}: PropsWithChildren<Props>) => {
const router = useRouter()
const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const { t } = useTranslation('common')
// Cookies
const cookie = getCookie('account')
const accountData: AccountCookie = cookie
? JSON.parse(cookie as string)
: null
const headers = accountData
? { Authorization: `Bearer ${accountData.token}` }
: {}
// State: Component
const [alertOpen, setAlertOpen] = useState(false)
const [formValid, setFormValid] = useState(false)
// State: Selects
const [weaponKey1Open, setWeaponKey1Open] = useState(false)
const [weaponKey2Open, setWeaponKey2Open] = useState(false)
const [weaponKey3Open, setWeaponKey3Open] = useState(false)
const [weaponKey4Open, setWeaponKey4Open] = useState(false)
const [ax1Open, setAx1Open] = useState(false)
const [ax2Open, setAx2Open] = 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
const headerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>()
// Hooks
// Set up modal data state when the gridWeapon changes
useEffect(() => {
setElement(gridWeapon.element)
if (gridWeapon.weapon_keys) {
gridWeapon.weapon_keys.forEach((key) => {
if (key.slot + 1 === 1) {
setWeaponKey1(key)
} else if (key.slot + 1 === 2) {
setWeaponKey2(key)
} else if (key.slot + 1 === 3) {
setWeaponKey3(key)
}
})
}
if (gridWeapon.awakening) {
setAwakening(gridWeapon.awakening.type)
setAwakeningLevel(gridWeapon.awakening.level)
}
if (gridWeapon.object.ax || gridWeapon.object.awakenings) {
setFormValid(false)
} else {
setFormValid(true)
}
}, [gridWeapon])
// Methods: Data retrieval
// Receive values from ElementToggle
function receiveElementValue(elementId: number) {
setElement(elementId)
}
// Receive values from AXSelect
function receiveAxValues(
primaryAxModifier: number,
primaryAxValue: number,
secondaryAxModifier: number,
secondaryAxValue: number
) {
setPrimaryAxModifier(primaryAxModifier)
setSecondaryAxModifier(secondaryAxModifier)
setPrimaryAxValue(primaryAxValue)
setSecondaryAxValue(secondaryAxValue)
}
// Receive values from AwakeningSelectWithInput
function receiveAwakeningValues(id: string, level: number) {
setAwakening(gridWeapon.object.awakenings.find((a) => a.id === id))
console.log(level)
setAwakeningLevel(level)
setFormValid(true)
}
// Receive values from WeaponKeySelect
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() {
let object: GridWeaponObject = { weapon: {} }
if (gridWeapon.object.element == 0) object.weapon.element = element
if ([2, 3, 17, 24].includes(gridWeapon.object.series) && weaponKey1) {
object.weapon.weapon_key1_id = weaponKey1.id
}
if ([2, 3, 17].includes(gridWeapon.object.series) && weaponKey2)
object.weapon.weapon_key2_id = weaponKey2.id
if (gridWeapon.object.series == 17 && weaponKey3)
object.weapon.weapon_key3_id = weaponKey3.id
if (gridWeapon.object.ax && gridWeapon.object.ax_type > 0) {
object.weapon.ax_modifier1 = primaryAxModifier
object.weapon.ax_modifier2 = secondaryAxModifier
object.weapon.ax_strength1 = primaryAxValue
object.weapon.ax_strength2 = secondaryAxValue
}
if (gridWeapon.object.awakenings) {
object.weapon.awakening_id = awakening?.id
object.weapon.awakening_level = awakeningLevel
}
return object
}
async function handleUpdateWeapon() {
await updateWeapon(prepareObject())
if (onOpenChange) onOpenChange(false)
}
// Methods: Event handlers
const anySelectOpen =
weaponKey1Open ||
weaponKey2Open ||
weaponKey3Open ||
weaponKey4Open ||
ax1Open ||
ax2Open ||
awakeningOpen
function openSelect(index: 1 | 2 | 3 | 4) {
setWeaponKey1Open(index === 1 ? !weaponKey1Open : false)
setWeaponKey2Open(index === 2 ? !weaponKey2Open : false)
setWeaponKey3Open(index === 3 ? !weaponKey3Open : false)
setWeaponKey4Open(index === 4 ? !weaponKey4Open : false)
}
function receiveAxOpen(index: 1 | 2, isOpen: boolean) {
if (index === 1) setAx1Open(isOpen)
if (index === 2) setAx2Open(isOpen)
}
function receiveAwakeningOpen(isOpen: boolean) {
setAwakeningOpen(isOpen)
}
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) {
if (modalOpen && hasBeenModified()) {
setAlertOpen(true)
} else {
onOpenChange(open)
}
}
function onEscapeKeyDown(event: KeyboardEvent) {
if (anySelectOpen) {
return event.preventDefault()
} else if (hasBeenModified()) {
setAlertOpen(true)
} else {
close()
}
}
// Methods: Modification checking
function hasBeenModified() {
return (
elementChanged() ||
weaponKeysChanged() ||
axChanged() ||
awakeningChanged()
)
}
function elementChanged() {
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
const ax2Changed =
gridWeapon.ax[1].modifier !== secondaryAxModifier ||
gridWeapon.ax[1].strength !== secondaryAxValue
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(
gridWeapon.awakening.type,
awakening,
gridWeapon.awakening.level,
awakeningLevel
)
// 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>
<h3>{t('modals.weapon.subtitles.weapon_keys')}</h3>
{[2, 3, 17, 22].includes(gridWeapon.object.series) ? (
<WeaponKeySelect
open={weaponKey1Open}
weaponKey={weaponKey1}
series={gridWeapon.object.series}
slot={0}
onOpenChange={() => openSelect(1)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey1Open(false)}
/>
) : (
''
)}
{[2, 3, 17].includes(gridWeapon.object.series) ? (
<WeaponKeySelect
open={weaponKey2Open}
weaponKey={weaponKey2}
series={gridWeapon.object.series}
slot={1}
onOpenChange={() => openSelect(2)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey2Open(false)}
/>
) : (
''
)}
{gridWeapon.object.series == 17 ? (
<WeaponKeySelect
open={weaponKey3Open}
weaponKey={weaponKey3}
series={gridWeapon.object.series}
slot={2}
onOpenChange={() => openSelect(3)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey3Open(false)}
/>
) : (
''
)}
{gridWeapon.object.series == 24 && gridWeapon.object.uncap.ulb ? (
<WeaponKeySelect
open={weaponKey4Open}
weaponKey={weaponKey1}
series={gridWeapon.object.series}
slot={0}
onOpenChange={() => openSelect(4)}
onChange={receiveWeaponKey}
onClose={() => setWeaponKey4Open(false)}
/>
) : (
''
)}
</section>
)
const axSelect = (
<section>
<h3>{t('modals.weapon.subtitles.ax_skills')}</h3>
<AXSelect
axType={gridWeapon.object.ax_type}
currentSkills={gridWeapon.ax}
onOpenChange={receiveAxOpen}
sendValidity={receiveValidity}
sendValues={receiveAxValues}
/>
</section>
)
const awakeningSelect = (
<section>
<h3>{t('modals.weapon.subtitles.awakening')}</h3>
<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}
/>
</section>
)
const confirmationAlert = (
<Alert
message={
<span>
<Trans i18nKey="alert.unsaved_changes.object">
You will lose all changes to{' '}
<strong>{{ objectName: gridWeapon.object.name[locale] }}</strong> if
you continue.
<br />
<br />
Are you sure you want to continue without saving?
</Trans>
</span>
}
open={alertOpen}
primaryActionText={t('alert.unsaved_changes.buttons.confirm')}
primaryAction={close}
cancelActionText={t('alert.unsaved_changes.buttons.cancel')}
cancelAction={() => setAlertOpen(false)}
/>
)
return (
<>
{confirmationAlert}
<Dialog open={modalOpen} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent
className="Weapon"
headerRef={headerRef}
footerRef={footerRef}
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={onEscapeKeyDown}
>
<DialogHeader
ref={headerRef}
title={gridWeapon.object.name[locale]}
subtitle={t('modals.characters.title')}
image={{
src: `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-square/${gridWeapon.object.granblue_id}.jpg`,
alt: gridWeapon.object.name[locale],
}}
/>
<section className={styles.mods}>
{gridWeapon.object.element == 0 && elementSelect}
{[2, 3, 17, 24].includes(gridWeapon.object.series) && keySelect}
{gridWeapon.object.ax && axSelect}
{gridWeapon.object.awakenings && awakeningSelect}
</section>
<DialogFooter
ref={footerRef}
rightElements={[
<Button
bound={true}
onClick={handleUpdateWeapon}
key="confirm"
disabled={!formValid}
text={t('modals.weapon.buttons.confirm')}
/>,
]}
/>
</DialogContent>
</Dialog>
</>
)
}
export default WeaponModal