import React, { useEffect, useState } from 'react' import { useRouter } from 'next/router' import { useSnapshot } from 'valtio' import { Trans, useTranslation } from 'next-i18next' import { AxiosResponse } from 'axios' import classNames from 'classnames' import cloneDeep from 'lodash.clonedeep' import Alert from '~components/common/Alert' import Button from '~components/common/Button' import CharacterHovercard from '~components/character/CharacterHovercard' import CharacterModal from '~components/character/CharacterModal' import { ContextMenu, ContextMenuTrigger, ContextMenuContent, } from '~components/common/ContextMenu' import ContextMenuItem from '~components/common/ContextMenuItem' import SearchModal from '~components/search/SearchModal' import UncapIndicator from '~components/uncap/UncapIndicator' import api from '~utils/api' import { appState } from '~utils/appState' import PlusIcon from '~public/icons/Add.svg' import SettingsIcon from '~public/icons/Settings.svg' // Types import type { CharacterOverMastery, GridCharacterObject, PerpetuityObject, SearchableObject, } from '~types' import styles from './index.module.scss' interface Props { gridCharacter?: GridCharacter position: number editable: boolean removeCharacter: (id: string) => void updateObject: (object: SearchableObject, position: number) => void updateUncap: (id: string, position: number, uncap: number) => void updateTranscendence: (id: string, position: number, stage: number) => void } const CharacterUnit = ({ gridCharacter, position, editable, removeCharacter: sendCharacterToRemove, updateObject, updateUncap, updateTranscendence, }: Props) => { // Translations and locale const { t } = useTranslation('common') const router = useRouter() const locale = router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' // State snapshot const { party, grid } = useSnapshot(appState) // State: UI const [detailsModalOpen, setDetailsModalOpen] = useState(false) const [searchModalOpen, setSearchModalOpen] = useState(false) const [contextMenuOpen, setContextMenuOpen] = useState(false) const [alertOpen, setAlertOpen] = useState(false) // State: Other const [imageUrl, setImageUrl] = useState('') // Classes const classes = classNames({ unit: true, [styles.unit]: true, [styles.editable]: editable, [styles.filled]: gridCharacter !== undefined, }) // Other const character = gridCharacter?.object // Hooks useEffect(() => { generateImageUrl() }) // Methods: Open layer function openCharacterModal(event: Event) { setDetailsModalOpen(true) } function openSearchModal() { if (editable) setSearchModalOpen(true) } function openRemoveCharacterAlert() { setAlertOpen(true) } // Methods: Handle button clicked function handleButtonClicked() { setContextMenuOpen(!contextMenuOpen) } function handlePerpetuityClick() { if (gridCharacter) { let object: PerpetuityObject = { character: { perpetuity: !gridCharacter.perpetuity }, } updateCharacter(object) } } // Methods: Handle open change function handleCharacterModalOpenChange(open: boolean) { setDetailsModalOpen(open) } function handleSearchModalOpenChange(open: boolean) { setSearchModalOpen(open) } function handleContextMenuOpenChange(open: boolean) { if (!open) setContextMenuOpen(false) } // Methods: Mutate data // Send the GridWeaponObject to the server async function updateCharacter( object: GridCharacterObject | PerpetuityObject ) { if (gridCharacter) return await api.endpoints.grid_characters .update(gridCharacter.id, object) .then((response) => processResult(response)) .catch((error) => processError(error)) } // Save the server's response to state function processResult(response: AxiosResponse) { const gridCharacter: GridCharacter = response.data let character = cloneDeep(gridCharacter) if (character.over_mastery) { const overMastery: CharacterOverMastery = { 1: gridCharacter.over_mastery[0], 2: gridCharacter.over_mastery[1], 3: gridCharacter.over_mastery[2], 4: gridCharacter.over_mastery[3], } character.over_mastery = overMastery } appState.grid.characters[gridCharacter.position] = character } function processError(error: any) { console.error(error) } function passUncapData(uncap: number) { if (gridCharacter) updateUncap(gridCharacter.id, position, uncap) } function passTranscendenceData(stage: number) { if (gridCharacter) updateTranscendence(gridCharacter.id, position, stage) } function removeCharacter() { if (gridCharacter) sendCharacterToRemove(gridCharacter.id) setAlertOpen(false) } // Methods: Image string generation function generateImageUrl() { let imgSrc = '' if (gridCharacter) { const character = gridCharacter.object! // Change the image based on the uncap level let suffix = '01' if (gridCharacter.transcendence_step > 0) suffix = '04' else if (gridCharacter.uncap_level >= 5) suffix = '03' else if (gridCharacter.uncap_level > 2) suffix = '02' // Special casing for Lyria (and Young Cat eventually) if (gridCharacter.object.granblue_id === '3030182000') { let element = 1 if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) { element = grid.weapons.mainWeapon.element } else if (party.element != 0) { element = party.element } suffix = `${suffix}_0${element}` } imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/character-main/${character.granblue_id}_${suffix}.jpg` } setImageUrl(imgSrc) } // Methods: Layer element rendering const characterModal = () => { if (gridCharacter) { return ( ) } } const contextMenu = () => { if (editable && gridCharacter && gridCharacter.id) { return ( <>