Add context menu to WeaponUnit

Also refactored and organized WeaponUnit and CharacterUnit
This commit is contained in:
Justin Edmund 2023-01-20 20:38:44 -08:00
parent d4e598e36b
commit 2be23b5ea0
4 changed files with 480 additions and 282 deletions

View file

@ -1,8 +1,8 @@
import React, { useEffect, useState } from 'react' import React, { MouseEvent, useEffect, useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { Trans, useTranslation } from 'next-i18next' import { Trans, useTranslation } from 'next-i18next'
import classnames from 'classnames' import classNames from 'classnames'
import Alert from '~components/Alert' import Alert from '~components/Alert'
import Button from '~components/Button' import Button from '~components/Button'
@ -30,52 +30,112 @@ interface Props {
gridCharacter?: GridCharacter gridCharacter?: GridCharacter
position: number position: number
editable: boolean editable: boolean
removeCharacter: (id: string) => void
updateObject: (object: SearchableObject, position: number) => void updateObject: (object: SearchableObject, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void updateUncap: (id: string, position: number, uncap: number) => void
removeCharacter: (id: string) => void
} }
const CharacterUnit = (props: Props) => { const CharacterUnit = ({
gridCharacter,
position,
editable,
removeCharacter: sendCharacterToRemove,
updateObject,
updateUncap,
}: Props) => {
// Translations and locale
const { t } = useTranslation('common') const { t } = useTranslation('common')
const { party, grid } = useSnapshot(appState)
const router = useRouter() const router = useRouter()
const locale = const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
const [imageUrl, setImageUrl] = useState('') // State snapshot
const { party, grid } = useSnapshot(appState)
const classes = classnames({ // State: UI
CharacterUnit: true, const [detailsModalOpen, setDetailsModalOpen] = useState(false)
editable: props.editable, const [searchModalOpen, setSearchModalOpen] = useState(false)
filled: props.gridCharacter !== undefined, const [contextMenuOpen, setContextMenuOpen] = useState(false)
})
const [modalOpen, setModalOpen] = useState(false)
const [alertOpen, setAlertOpen] = useState(false) const [alertOpen, setAlertOpen] = useState(false)
const gridCharacter = props.gridCharacter // State: Other
const [imageUrl, setImageUrl] = useState('')
// Classes
const classes = classNames({
CharacterUnit: true,
editable: editable,
filled: gridCharacter !== undefined,
})
const buttonClasses = classNames({
Options: true,
Clicked: contextMenuOpen,
})
// Other
const character = gridCharacter?.object const character = gridCharacter?.object
// Hooks
useEffect(() => { useEffect(() => {
generateImageUrl() generateImageUrl()
}) })
// Methods: Open layer
function openCharacterModal(event: Event) {
setDetailsModalOpen(true)
}
function openSearchModal(event: MouseEvent<HTMLDivElement>) {
if (editable) setSearchModalOpen(true)
}
function openRemoveCharacterAlert() {
setAlertOpen(true)
}
// Methods: Handle button clicked
function handleButtonClicked() {
setContextMenuOpen(!contextMenuOpen)
}
// 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
function passUncapData(uncap: number) {
if (gridCharacter) updateUncap(gridCharacter.id, position, uncap)
}
function removeCharacter() {
if (gridCharacter) sendCharacterToRemove(gridCharacter.id)
}
// Methods: Image string generation
function generateImageUrl() { function generateImageUrl() {
let imgSrc = '' let imgSrc = ''
if (props.gridCharacter) { if (gridCharacter) {
const character = props.gridCharacter.object! const character = gridCharacter.object!
// Change the image based on the uncap level // Change the image based on the uncap level
let suffix = '01' let suffix = '01'
if (props.gridCharacter.uncap_level == 6) suffix = '04' if (gridCharacter.uncap_level == 6) suffix = '04'
else if (props.gridCharacter.uncap_level == 5) suffix = '03' else if (gridCharacter.uncap_level == 5) suffix = '03'
else if (props.gridCharacter.uncap_level > 2) suffix = '02' else if (gridCharacter.uncap_level > 2) suffix = '02'
// Special casing for Lyria (and Young Cat eventually) // Special casing for Lyria (and Young Cat eventually)
if (props.gridCharacter.object.granblue_id === '3030182000') { if (gridCharacter.object.granblue_id === '3030182000') {
let element = 1 let element = 1
if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) { if (grid.weapons.mainWeapon && grid.weapons.mainWeapon.element) {
element = grid.weapons.mainWeapon.element element = grid.weapons.mainWeapon.element
@ -92,49 +152,45 @@ const CharacterUnit = (props: Props) => {
setImageUrl(imgSrc) setImageUrl(imgSrc)
} }
function passUncapData(uncap: number) { // Methods: Layer element rendering
if (props.gridCharacter) const characterModal = () => {
props.updateUncap(props.gridCharacter.id, props.position, uncap) if (gridCharacter) {
} return (
<CharacterModal
const image = ( gridCharacter={gridCharacter}
<div className="CharacterImage"> open={detailsModalOpen}
<img alt={character?.name.en} className="grid_image" src={imageUrl} /> onOpenChange={handleCharacterModalOpenChange}
{props.editable ? ( />
<span className="icon">
<PlusIcon />
</span>
) : (
''
)}
</div>
) )
}
}
const editableImage = ( const contextMenu = () => {
<SearchModal if (editable && gridCharacter && gridCharacter.id) {
placeholderText={t('search.placeholders.character')} return (
fromPosition={props.position} <>
object="characters" <ContextMenu onOpenChange={handleContextMenuOpenChange}>
send={props.updateObject} <ContextMenuTrigger asChild>
> <Button
{image} accessoryIcon={<SettingsIcon />}
</SearchModal> className={buttonClasses}
onClick={handleButtonClicked}
/>
</ContextMenuTrigger>
<ContextMenuContent align="start">
<ContextMenuItem onSelect={openCharacterModal}>
Modify character
</ContextMenuItem>
<ContextMenuItem onSelect={openRemoveCharacterAlert}>
Remove from grid
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
{characterModal()}
{removeAlert()}
</>
) )
function openCharacterModal(event: Event) {
setModalOpen(true)
} }
function onCharacterModalOpenChange(open: boolean) {
setModalOpen(open)
}
function openRemoveCharacterAlert() {
setAlertOpen(true)
}
function removeCharacter() {
if (gridCharacter) props.removeCharacter(gridCharacter.id)
} }
const removeAlert = () => { const removeAlert = () => {
@ -156,40 +212,40 @@ const CharacterUnit = (props: Props) => {
) )
} }
const contextMenu = () => { const searchModal = () => {
if (props.editable && gridCharacter && gridCharacter.id) { if (editable) {
return ( return (
<> <SearchModal
<ContextMenu> placeholderText={t('search.placeholders.character')}
<ContextMenuTrigger asChild> fromPosition={position}
<Button accessoryIcon={<SettingsIcon />} className="Options" /> object="characters"
</ContextMenuTrigger> open={searchModalOpen}
<ContextMenuContent align="start"> onOpenChange={handleSearchModalOpenChange}
<ContextMenuItem onSelect={openCharacterModal}> send={updateObject}
Modify character
</ContextMenuItem>
<ContextMenuItem onSelect={openRemoveCharacterAlert}>
Remove from grid
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
<CharacterModal
gridCharacter={gridCharacter}
open={modalOpen}
onOpenChange={onCharacterModalOpenChange}
/> />
{removeAlert()}
</>
) )
} }
} }
// // Methods: Core element rendering
const image = (
<div className="CharacterImage" onClick={openSearchModal}>
<img alt={character?.name.en} className="grid_image" src={imageUrl} />
{editable ? (
<span className="icon">
<PlusIcon />
</span>
) : (
''
)}
</div>
)
const unitContent = ( const unitContent = (
<>
<div className={classes}> <div className={classes}>
{contextMenu()} {contextMenu()}
{props.editable ? editableImage : image} {image}
{gridCharacter && character ? ( {gridCharacter && character ? (
<UncapIndicator <UncapIndicator
type="character" type="character"
@ -204,15 +260,17 @@ const CharacterUnit = (props: Props) => {
)} )}
<h3 className="CharacterName">{character?.name[locale]}</h3> <h3 className="CharacterName">{character?.name[locale]}</h3>
</div> </div>
{searchModal()}
</>
) )
const withHovercard = ( const unitContentWithHovercard = (
<CharacterHovercard gridCharacter={gridCharacter!}> <CharacterHovercard gridCharacter={gridCharacter!}>
{unitContent} {unitContent}
</CharacterHovercard> </CharacterHovercard>
) )
return gridCharacter && !props.editable ? withHovercard : unitContent return gridCharacter && !editable ? unitContentWithHovercard : unitContent
} }
export default CharacterUnit export default CharacterUnit

View file

@ -1,4 +1,4 @@
import React, { 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 { useTranslation } from 'next-i18next'
@ -40,10 +40,16 @@ interface GridWeaponObject {
interface Props { interface Props {
gridWeapon: GridWeapon gridWeapon: GridWeapon
children: React.ReactNode open: boolean
onOpenChange: (open: boolean) => void
} }
const WeaponModal = ({ gridWeapon, children }: Props) => { const WeaponModal = ({
gridWeapon,
open: modalOpen,
children,
onOpenChange,
}: PropsWithChildren<Props>) => {
const router = useRouter() const router = useRouter()
const locale = const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
@ -87,6 +93,10 @@ const WeaponModal = ({ gridWeapon, children }: Props) => {
const [ax2Open, setAx2Open] = useState(false) const [ax2Open, setAx2Open] = useState(false)
const [awakeningOpen, setAwakeningOpen] = useState(false) const [awakeningOpen, setAwakeningOpen] = useState(false)
useEffect(() => {
setOpen(modalOpen)
}, [modalOpen])
useEffect(() => { useEffect(() => {
setElement(gridWeapon.element) setElement(gridWeapon.element)
@ -308,13 +318,14 @@ const WeaponModal = ({ gridWeapon, children }: Props) => {
) )
} }
function openChange(open: boolean) { function handleOpenChange(open: boolean) {
if (gridWeapon.object.ax || gridWeapon.object.awakening) { if (gridWeapon.object.ax || gridWeapon.object.awakening) {
setFormValid(false) setFormValid(false)
} else { } else {
setFormValid(true) setFormValid(true)
} }
setOpen(open) setOpen(open)
onOpenChange(open)
} }
const anySelectOpen = const anySelectOpen =
@ -336,7 +347,7 @@ const WeaponModal = ({ gridWeapon, children }: Props) => {
return ( return (
// TODO: Refactor into Dialog component // TODO: Refactor into Dialog component
<Dialog open={open} onOpenChange={openChange}> <Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger asChild>{children}</DialogTrigger> <DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent <DialogContent
className="Weapon" className="Weapon"

View file

@ -12,8 +12,16 @@
min-height: auto; min-height: auto;
} }
&:hover .Button { .Button {
display: block; pointer-events: none;
opacity: 0;
z-index: 10;
}
&:hover .Button,
.Button.Clicked {
pointer-events: initial;
opacity: 1;
} }
&.editable .WeaponImage:hover { &.editable .WeaponImage:hover {
@ -95,15 +103,6 @@
display: none; display: none;
} }
.Button {
box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.14);
display: none;
position: absolute;
left: $unit;
top: $unit;
z-index: 10;
}
h3 { h3 {
color: var(--text-primary); color: var(--text-primary);
font-size: $font-button; font-size: $font-button;

View file

@ -1,10 +1,17 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState, MouseEvent } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'next-i18next' import { Trans, useTranslation } from 'next-i18next'
import classNames from 'classnames' import classNames from 'classnames'
import Alert from '~components/Alert'
import SearchModal from '~components/SearchModal' import SearchModal from '~components/SearchModal'
import WeaponModal from '~components/WeaponModal' import WeaponModal from '~components/WeaponModal'
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
} from '~components/ContextMenu'
import ContextMenuItem from '~components/ContextMenuItem'
import WeaponHovercard from '~components/WeaponHovercard' import WeaponHovercard from '~components/WeaponHovercard'
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from '~components/UncapIndicator'
import Button from '~components/Button' import Button from '~components/Button'
@ -23,48 +30,147 @@ interface Props {
unitType: 0 | 1 unitType: 0 | 1
position: number position: number
editable: boolean editable: boolean
removeWeapon: (id: string) => void
updateObject: (object: SearchableObject, position: number) => void updateObject: (object: SearchableObject, position: number) => void
updateUncap: (id: string, position: number, uncap: number) => void updateUncap: (id: string, position: number, uncap: number) => void
} }
const WeaponUnit = (props: Props) => { const WeaponUnit = ({
gridWeapon,
unitType,
position,
editable,
removeWeapon: sendWeaponToRemove,
updateObject,
updateUncap,
}: Props) => {
// Translations and locale
const { t } = useTranslation('common') const { t } = useTranslation('common')
const [imageUrl, setImageUrl] = useState('')
const router = useRouter() const router = useRouter()
const locale = const locale =
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en' router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
// 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({ const classes = classNames({
WeaponUnit: true, WeaponUnit: true,
mainhand: props.unitType == 0, mainhand: unitType == 0,
grid: props.unitType == 1, grid: unitType == 1,
editable: props.editable, editable: editable,
filled: props.gridWeapon !== undefined, filled: gridWeapon !== undefined,
empty: props.gridWeapon == undefined, empty: gridWeapon == undefined,
}) })
const gridWeapon = props.gridWeapon const buttonClasses = classNames({
Options: true,
Clicked: contextMenuOpen,
})
// Other
const weapon = gridWeapon?.object const weapon = gridWeapon?.object
// Hooks
useEffect(() => { useEffect(() => {
generateImageUrl() generateImageUrl()
}) })
// Methods: Convenience
function canBeModified(gridWeapon: GridWeapon) {
const weapon = gridWeapon.object
return (
weapon.ax ||
weapon.awakening ||
(weapon.series && [2, 3, 17, 22, 24].includes(weapon.series))
)
}
// Methods: Open layer
function openWeaponModal(event: Event) {
setDetailsModalOpen(true)
}
function openSearchModal(event: MouseEvent<HTMLDivElement>) {
if (editable) setSearchModalOpen(true)
}
function openRemoveWeaponAlert() {
setAlertOpen(true)
}
// Methods: Handle button clicked
function handleButtonClicked() {
setContextMenuOpen(!contextMenuOpen)
}
// Methods: Handle open change
function handleContextMenuOpenChange(open: boolean) {
if (!open) setContextMenuOpen(false)
}
function handleWeaponModalOpenChange(open: boolean) {
setDetailsModalOpen(open)
}
function handleSearchModalOpenChange(open: boolean) {
setSearchModalOpen(open)
}
// Methods: Mutate data
function passUncapData(index: number) {
if (gridWeapon) updateUncap(gridWeapon.id, position, index)
}
function removeWeapon() {
if (gridWeapon) sendWeaponToRemove(gridWeapon.id)
}
// Methods: Data fetching and manipulation
function getCanonicalAxSkill(index: number) {
if (
gridWeapon &&
gridWeapon.object.ax &&
gridWeapon.object.ax_type > 0 &&
gridWeapon.ax
) {
const axOptions = ax[gridWeapon.object.ax_type - 1]
const weaponAxSkill: SimpleAxSkill = gridWeapon.ax[0]
let axSkill = axOptions.find((ax) => ax.id === weaponAxSkill.modifier)
if (index !== 0 && axSkill && axSkill.secondary) {
const weaponSubAxSkill: SimpleAxSkill = gridWeapon.ax[1]
axSkill = axSkill.secondary.find(
(ax) => ax.id === weaponSubAxSkill.modifier
)
}
return axSkill
} else return
}
// Methods: Image string generation
function generateImageUrl() { function generateImageUrl() {
let imgSrc = '' let imgSrc = ''
if (props.gridWeapon) { if (gridWeapon) {
const weapon = props.gridWeapon.object! const weapon = gridWeapon.object!
if (props.unitType == 0) { if (unitType == 0) {
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element) if (gridWeapon.object.element == 0 && gridWeapon.element)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}_${props.gridWeapon.element}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}_${gridWeapon.element}.jpg`
else else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-main/${weapon.granblue_id}.jpg`
} else { } else {
if (props.gridWeapon.object.element == 0 && props.gridWeapon.element) if (gridWeapon.object.element == 0 && gridWeapon.element)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${props.gridWeapon.element}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}_${gridWeapon.element}.jpg`
else else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/weapon-grid/${weapon.granblue_id}.jpg`
} }
@ -74,29 +180,30 @@ const WeaponUnit = (props: Props) => {
} }
function placeholderImageUrl() { function placeholderImageUrl() {
return props.unitType == 0 return unitType == 0
? '/images/placeholders/placeholder-weapon-main.png' ? '/images/placeholders/placeholder-weapon-main.png'
: '/images/placeholders/placeholder-weapon-grid.png' : '/images/placeholders/placeholder-weapon-grid.png'
} }
// Methods: Image element rendering
function awakeningImage() { function awakeningImage() {
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.awakening && gridWeapon.object.awakening &&
props.gridWeapon.awakening && gridWeapon.awakening &&
props.gridWeapon.awakening.type > 0 && gridWeapon.awakening.type > 0 &&
props.gridWeapon.awakening.type != null gridWeapon.awakening.type != null
) { ) {
const awakening = weaponAwakening.find( const awakening = weaponAwakening.find(
(awakening) => awakening.id === props.gridWeapon?.awakening?.type (awakening) => awakening.id === gridWeapon?.awakening?.type
) )
const name = awakening?.name[locale] const name = awakening?.name[locale]
return ( return (
<img <img
alt={`${name} Lv${props.gridWeapon.awakening.level}`} alt={`${name} Lv${gridWeapon.awakening.level}`}
className="Awakening" className="Awakening"
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/weapon_${props.gridWeapon.awakening.type}.png`} src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/awakening/weapon_${gridWeapon.awakening.type}.png`}
/> />
) )
} }
@ -109,18 +216,18 @@ const WeaponUnit = (props: Props) => {
// If there is a grid weapon, it is a Draconic Weapon and it has keys // If there is a grid weapon, it is a Draconic Weapon and it has keys
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.series === 3 && gridWeapon.object.series === 3 &&
props.gridWeapon.weapon_keys gridWeapon.weapon_keys
) { ) {
if (index === 0 && props.gridWeapon.weapon_keys[0]) { if (index === 0 && gridWeapon.weapon_keys[0]) {
altText = `${props.gridWeapon.weapon_keys[0].name[locale]}` altText = `${gridWeapon.weapon_keys[0].name[locale]}`
filename = `${props.gridWeapon.weapon_keys[0].slug}.png` filename = `${gridWeapon.weapon_keys[0].slug}.png`
} else if (index === 1 && props.gridWeapon.weapon_keys[1]) { } else if (index === 1 && gridWeapon.weapon_keys[1]) {
altText = `${props.gridWeapon.weapon_keys[1].name[locale]}` altText = `${gridWeapon.weapon_keys[1].name[locale]}`
const element = props.gridWeapon.object.element const element = gridWeapon.object.element
filename = `${props.gridWeapon.weapon_keys[1].slug}-${element}.png` filename = `${gridWeapon.weapon_keys[1].slug}-${element}.png`
} }
return ( return (
@ -137,12 +244,12 @@ const WeaponUnit = (props: Props) => {
function telumaImages() { function telumaImages() {
let images: JSX.Element[] = [] let images: JSX.Element[] = []
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.series === 3 && gridWeapon.object.series === 3 &&
props.gridWeapon.weapon_keys && gridWeapon.weapon_keys &&
props.gridWeapon.weapon_keys.length > 0 gridWeapon.weapon_keys.length > 0
) { ) {
for (let i = 0; i < props.gridWeapon.weapon_keys.length; i++) { for (let i = 0; i < gridWeapon.weapon_keys.length; i++) {
const image = telumaImage(i) const image = telumaImage(i)
if (image) images.push(image) if (image) images.push(image)
} }
@ -158,27 +265,27 @@ const WeaponUnit = (props: Props) => {
// If there is a grid weapon, it is a Dark Opus Weapon and it has keys // If there is a grid weapon, it is a Dark Opus Weapon and it has keys
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.series === 17 && gridWeapon.object.series === 17 &&
props.gridWeapon.weapon_keys gridWeapon.weapon_keys
) { ) {
if ( if (
props.gridWeapon.weapon_keys[index] && gridWeapon.weapon_keys[index] &&
(props.gridWeapon.weapon_keys[index].slot === 1 || (gridWeapon.weapon_keys[index].slot === 1 ||
props.gridWeapon.weapon_keys[index].slot === 2) gridWeapon.weapon_keys[index].slot === 2)
) { ) {
altText = `${props.gridWeapon.weapon_keys[index].name[locale]}` altText = `${gridWeapon.weapon_keys[index].name[locale]}`
filename = `${props.gridWeapon.weapon_keys[index].slug}.png` filename = `${gridWeapon.weapon_keys[index].slug}.png`
} else if ( } else if (
props.gridWeapon.weapon_keys[index] && gridWeapon.weapon_keys[index] &&
props.gridWeapon.weapon_keys[index].slot === 0 gridWeapon.weapon_keys[index].slot === 0
) { ) {
altText = `${props.gridWeapon.weapon_keys[index].name[locale]}` altText = `${gridWeapon.weapon_keys[index].name[locale]}`
const weapon = props.gridWeapon.object.proficiency const weapon = gridWeapon.object.proficiency
const suffix = `${weapon}` const suffix = `${weapon}`
filename = `${props.gridWeapon.weapon_keys[index].slug}-${suffix}.png` filename = `${gridWeapon.weapon_keys[index].slug}-${suffix}.png`
} }
} }
@ -195,12 +302,12 @@ const WeaponUnit = (props: Props) => {
function ultimaImages() { function ultimaImages() {
let images: JSX.Element[] = [] let images: JSX.Element[] = []
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.series === 17 && gridWeapon.object.series === 17 &&
props.gridWeapon.weapon_keys && gridWeapon.weapon_keys &&
props.gridWeapon.weapon_keys.length > 0 gridWeapon.weapon_keys.length > 0
) { ) {
for (let i = 0; i < props.gridWeapon.weapon_keys.length; i++) { for (let i = 0; i < gridWeapon.weapon_keys.length; i++) {
const image = ultimaImage(i) const image = ultimaImage(i)
if (image) images.push(image) if (image) images.push(image)
} }
@ -216,29 +323,29 @@ const WeaponUnit = (props: Props) => {
// If there is a grid weapon, it is a Dark Opus Weapon and it has keys // If there is a grid weapon, it is a Dark Opus Weapon and it has keys
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.series === 2 && gridWeapon.object.series === 2 &&
props.gridWeapon.weapon_keys gridWeapon.weapon_keys
) { ) {
if ( if (
props.gridWeapon.weapon_keys[index] && gridWeapon.weapon_keys[index] &&
props.gridWeapon.weapon_keys[index].slot === 0 gridWeapon.weapon_keys[index].slot === 0
) { ) {
altText = `${props.gridWeapon.weapon_keys[index].name[locale]}` altText = `${gridWeapon.weapon_keys[index].name[locale]}`
filename = `${props.gridWeapon.weapon_keys[index].slug}.png` filename = `${gridWeapon.weapon_keys[index].slug}.png`
} else if ( } else if (
props.gridWeapon.weapon_keys[index] && gridWeapon.weapon_keys[index] &&
props.gridWeapon.weapon_keys[index].slot === 1 gridWeapon.weapon_keys[index].slot === 1
) { ) {
altText = `${props.gridWeapon.weapon_keys[index].name[locale]}` altText = `${gridWeapon.weapon_keys[index].name[locale]}`
const element = props.gridWeapon.object.element const element = gridWeapon.object.element
const mod = props.gridWeapon.object.name.en.includes('Repudiation') const mod = gridWeapon.object.name.en.includes('Repudiation')
? 'primal' ? 'primal'
: 'magna' : 'magna'
const suffix = `${mod}-${element}` const suffix = `${mod}-${element}`
const weaponKey = props.gridWeapon.weapon_keys[index] const weaponKey = gridWeapon.weapon_keys[index]
if ( if (
[ [
@ -250,9 +357,9 @@ const WeaponUnit = (props: Props) => {
'chain-glorification', 'chain-glorification',
].includes(weaponKey.slug) ].includes(weaponKey.slug)
) { ) {
filename = `${props.gridWeapon.weapon_keys[index].slug}-${suffix}.png` filename = `${gridWeapon.weapon_keys[index].slug}-${suffix}.png`
} else { } else {
filename = `${props.gridWeapon.weapon_keys[index].slug}.png` filename = `${gridWeapon.weapon_keys[index].slug}.png`
} }
} }
@ -270,12 +377,12 @@ const WeaponUnit = (props: Props) => {
function opusImages() { function opusImages() {
let images: JSX.Element[] = [] let images: JSX.Element[] = []
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.series === 2 && gridWeapon.object.series === 2 &&
props.gridWeapon.weapon_keys && gridWeapon.weapon_keys &&
props.gridWeapon.weapon_keys.length > 0 gridWeapon.weapon_keys.length > 0
) { ) {
for (let i = 0; i < props.gridWeapon.weapon_keys.length; i++) { for (let i = 0; i < gridWeapon.weapon_keys.length; i++) {
const image = opusImage(i) const image = opusImage(i)
if (image) images.push(image) if (image) images.push(image)
} }
@ -288,13 +395,13 @@ const WeaponUnit = (props: Props) => {
const axSkill = getCanonicalAxSkill(index) const axSkill = getCanonicalAxSkill(index)
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.ax && gridWeapon.object.ax &&
props.gridWeapon.object.ax_type > 0 && gridWeapon.object.ax_type > 0 &&
props.gridWeapon.ax && gridWeapon.ax &&
axSkill axSkill
) { ) {
const altText = `${axSkill.name[locale]} Lv${props.gridWeapon.ax[index].strength}` const altText = `${axSkill.name[locale]} Lv${gridWeapon.ax[index].strength}`
return ( return (
<img <img
alt={altText} alt={altText}
@ -309,12 +416,12 @@ const WeaponUnit = (props: Props) => {
function axImages() { function axImages() {
let images: JSX.Element[] = [] let images: JSX.Element[] = []
if ( if (
props.gridWeapon && gridWeapon &&
props.gridWeapon.object.ax && gridWeapon.object.ax &&
props.gridWeapon.ax && gridWeapon.ax &&
props.gridWeapon.ax.length > 0 gridWeapon.ax.length > 0
) { ) {
for (let i = 0; i < props.gridWeapon.ax.length; i++) { for (let i = 0; i < gridWeapon.ax.length; i++) {
const image = axImage(i) const image = axImage(i)
if (image) images.push(image) if (image) images.push(image)
} }
@ -323,46 +430,86 @@ const WeaponUnit = (props: Props) => {
return images return images
} }
function getCanonicalAxSkill(index: number) { // Methods: Layer element rendering
if ( const weaponModal = () => {
props.gridWeapon && if (gridWeapon) {
props.gridWeapon.object.ax &&
props.gridWeapon.object.ax_type > 0 &&
props.gridWeapon.ax
) {
const axOptions = ax[props.gridWeapon.object.ax_type - 1]
const weaponAxSkill: SimpleAxSkill = props.gridWeapon.ax[0]
let axSkill = axOptions.find((ax) => ax.id === weaponAxSkill.modifier)
if (index !== 0 && axSkill && axSkill.secondary) {
const weaponSubAxSkill: SimpleAxSkill = props.gridWeapon.ax[1]
axSkill = axSkill.secondary.find(
(ax) => ax.id === weaponSubAxSkill.modifier
)
}
return axSkill
} else return
}
function passUncapData(index: number) {
if (props.gridWeapon)
props.updateUncap(props.gridWeapon.id, props.position, index)
}
function canBeModified(gridWeapon: GridWeapon) {
const weapon = gridWeapon.object
return ( return (
weapon.ax || <WeaponModal
weapon.awakening || gridWeapon={gridWeapon}
(weapon.series && [2, 3, 17, 22, 24].includes(weapon.series)) open={detailsModalOpen}
onOpenChange={handleWeaponModalOpenChange}
/>
)
}
}
const contextMenu = () => {
if (editable && gridWeapon && gridWeapon.id) {
return (
<>
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
<ContextMenuTrigger asChild>
<Button
accessoryIcon={<SettingsIcon />}
className={buttonClasses}
onClick={handleButtonClicked}
/>
</ContextMenuTrigger>
<ContextMenuContent align="start">
{canBeModified(gridWeapon) ? (
<ContextMenuItem onSelect={openWeaponModal}>
Modify weapon
</ContextMenuItem>
) : (
''
)}
<ContextMenuItem onSelect={openRemoveWeaponAlert}>
Remove from grid
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
{weaponModal()}
{removeAlert()}
</>
)
}
}
const removeAlert = () => {
return (
<Alert
open={alertOpen}
primaryAction={removeWeapon}
primaryActionText={t('modals.weapons.buttons.remove')}
cancelAction={() => setAlertOpen(false)}
cancelActionText={t('buttons.cancel')}
message={
<Trans i18nKey="modals.characters.messages.remove">
Are you sure you want to remove{' '}
<strong>{{ weapon: gridWeapon?.object.name[locale] }}</strong> from
your team?
</Trans>
}
/>
) )
} }
const searchModal = () => {
return (
<SearchModal
placeholderText={t('search.placeholders.weapon')}
fromPosition={position}
object="weapons"
open={searchModalOpen}
onOpenChange={handleSearchModalOpenChange}
send={updateObject}
/>
)
}
// Methods: Core element rendering
const image = ( const image = (
<div className="WeaponImage"> <div className="WeaponImage" onClick={openSearchModal}>
<div className="Modifiers"> <div className="Modifiers">
{awakeningImage()} {awakeningImage()}
<div className="Skills"> <div className="Skills">
@ -380,7 +527,7 @@ const WeaponUnit = (props: Props) => {
})} })}
src={imageUrl !== '' ? imageUrl : placeholderImageUrl()} src={imageUrl !== '' ? imageUrl : placeholderImageUrl()}
/> />
{props.editable ? ( {editable ? (
<span className="icon"> <span className="icon">
<PlusIcon /> <PlusIcon />
</span> </span>
@ -390,31 +537,12 @@ const WeaponUnit = (props: Props) => {
</div> </div>
) )
const editableImage = (
<SearchModal
placeholderText={t('search.placeholders.weapon')}
fromPosition={props.position}
object="weapons"
send={props.updateObject}
>
{image}
</SearchModal>
)
const unitContent = ( const unitContent = (
<>
<div className={classes}> <div className={classes}>
{props.editable && {contextMenu()}
gridWeapon && {image}
gridWeapon.id && {gridWeapon && weapon ? (
canBeModified(gridWeapon) ? (
<WeaponModal gridWeapon={gridWeapon}>
<Button accessoryIcon={<SettingsIcon />} />
</WeaponModal>
) : (
''
)}
{props.editable ? editableImage : image}
{gridWeapon ? (
<UncapIndicator <UncapIndicator
type="weapon" type="weapon"
ulb={gridWeapon.object.uncap.ulb || false} ulb={gridWeapon.object.uncap.ulb || false}
@ -428,13 +556,15 @@ const WeaponUnit = (props: Props) => {
)} )}
<h3 className="WeaponName">{weapon?.name[locale]}</h3> <h3 className="WeaponName">{weapon?.name[locale]}</h3>
</div> </div>
{searchModal()}
</>
) )
const withHovercard = ( const unitContentWithHovercard = (
<WeaponHovercard gridWeapon={gridWeapon!}>{unitContent}</WeaponHovercard> <WeaponHovercard gridWeapon={gridWeapon!}>{unitContent}</WeaponHovercard>
) )
return gridWeapon && !props.editable ? withHovercard : unitContent return gridWeapon && !editable ? unitContentWithHovercard : unitContent
} }
export default WeaponUnit export default WeaponUnit