Merge pull request #154 from jedmund/summon-renewal

Update features of Summon grid
This commit is contained in:
Justin Edmund 2023-01-22 21:34:16 -08:00 committed by GitHub
commit 68cc1b666d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 247 additions and 58 deletions

View file

@ -230,7 +230,11 @@ const CharacterUnit = ({
// Methods: Core element rendering // Methods: Core element rendering
const image = ( const image = (
<div className="CharacterImage" onClick={openSearchModal}> <div className="CharacterImage" onClick={openSearchModal}>
<img alt={character?.name.en} className="grid_image" src={imageUrl} /> <img
alt={character?.name[locale]}
className="grid_image"
src={imageUrl}
/>
{editable ? ( {editable ? (
<span className="icon"> <span className="icon">
<PlusIcon /> <PlusIcon />

View file

@ -11,6 +11,7 @@ interface Props {
exists: boolean exists: boolean
found?: boolean found?: boolean
offset: number offset: number
removeSummon: (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
} }
@ -31,6 +32,7 @@ const ExtraSummons = (props: Props) => {
editable={props.editable} editable={props.editable}
position={props.offset + i} position={props.offset + i}
unitType={1} unitType={1}
removeSummon={props.removeSummon}
gridSummon={props.grid[props.offset + i]} gridSummon={props.grid[props.offset + i]}
updateObject={props.updateObject} updateObject={props.updateObject}
updateUncap={props.updateUncap} updateUncap={props.updateUncap}

View file

@ -93,9 +93,37 @@ const SummonGrid = (props: Props) => {
}) })
} else { } else {
if (party.editable) if (party.editable)
saveSummon(party.id, summon, position).then((response) => saveSummon(party.id, summon, position)
storeGridSummon(response.data) .then((response) => handleSummonResponse(response.data))
) .catch((error) => {
const code = error.response.status
const data = error.response.data
if (code === 422) {
if (data.code === 'incompatible_summon_for_position') {
// setShowIncompatibleAlert(true)
}
}
})
}
}
async function handleSummonResponse(data: any) {
if (data.hasOwnProperty('errors')) {
} else {
storeGridSummon(data.grid_summon)
// If we replaced an existing weapon, remove it from the grid
if (data.hasOwnProperty('meta') && data.meta['replaced'] !== undefined) {
const position = data.meta['replaced']
if (position == -1) {
appState.grid.summons.mainSummon = undefined
} else if (position == 6) {
appState.grid.summons.friendSummon = undefined
} else {
appState.grid.summons.allSummons[position] = undefined
}
}
} }
} }
@ -209,6 +237,23 @@ const SummonGrid = (props: Props) => {
setPreviousUncapValues(newPreviousValues) setPreviousUncapValues(newPreviousValues)
} }
async function removeSummon(id: string) {
try {
const response = await api.endpoints.grid_summons.destroy({ id: id })
const data = response.data
if (data.position === -1) {
appState.grid.summons.mainSummon = undefined
} else if (data.position === 6) {
appState.grid.summons.friendSummon = undefined
} else {
appState.grid.summons.allSummons[response.data.position] = undefined
}
} catch (error) {
console.error(error)
}
}
// Render: JSX components // Render: JSX components
const mainSummonElement = ( const mainSummonElement = (
<div className="LabeledUnit"> <div className="LabeledUnit">
@ -219,6 +264,7 @@ const SummonGrid = (props: Props) => {
key="grid_main_summon" key="grid_main_summon"
position={-1} position={-1}
unitType={0} unitType={0}
removeSummon={removeSummon}
updateObject={receiveSummonFromSearch} updateObject={receiveSummonFromSearch}
updateUncap={initiateUncapUpdate} updateUncap={initiateUncapUpdate}
/> />
@ -251,6 +297,7 @@ const SummonGrid = (props: Props) => {
editable={party.editable} editable={party.editable}
position={i} position={i}
unitType={1} unitType={1}
removeSummon={removeSummon}
updateObject={receiveSummonFromSearch} updateObject={receiveSummonFromSearch}
updateUncap={initiateUncapUpdate} updateUncap={initiateUncapUpdate}
/> />
@ -266,6 +313,7 @@ const SummonGrid = (props: Props) => {
editable={party.editable} editable={party.editable}
exists={false} exists={false}
offset={numSummons} offset={numSummons}
removeSummon={removeSummon}
updateObject={receiveSummonFromSearch} updateObject={receiveSummonFromSearch}
updateUncap={initiateUncapUpdate} updateUncap={initiateUncapUpdate}
/> />

View file

@ -2,6 +2,20 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 4px; gap: 4px;
position: relative;
z-index: 0;
.Button {
pointer-events: none;
opacity: 0;
z-index: 10;
}
&:hover .Button,
.Button.Clicked {
pointer-events: initial;
opacity: 1;
}
&.grid { &.grid {
// max-width: 148px; // max-width: 148px;

View file

@ -1,15 +1,24 @@
import React, { useEffect, useState } from 'react' import React, { MouseEvent, useEffect, useState } 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 Button from '~components/Button'
import {
ContextMenu,
ContextMenuTrigger,
ContextMenuContent,
} from '~components/ContextMenu'
import ContextMenuItem from '~components/ContextMenuItem'
import SearchModal from '~components/SearchModal' import SearchModal from '~components/SearchModal'
import SummonHovercard from '~components/SummonHovercard' import SummonHovercard from '~components/SummonHovercard'
import UncapIndicator from '~components/UncapIndicator' import UncapIndicator from '~components/UncapIndicator'
import PlusIcon from '~public/icons/Add.svg'
import type { SearchableObject } from '~types' import type { SearchableObject } from '~types'
import PlusIcon from '~public/icons/Add.svg'
import SettingsIcon from '~public/icons/Settings.svg'
import './index.scss' import './index.scss'
interface Props { interface Props {
@ -17,39 +26,94 @@ interface Props {
unitType: 0 | 1 | 2 unitType: 0 | 1 | 2
position: number position: number
editable: boolean editable: boolean
removeSummon: (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 SummonUnit = (props: Props) => { const SummonUnit = ({
gridSummon,
unitType,
position,
editable,
removeSummon: sendSummonToRemove,
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 [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({
SummonUnit: true, SummonUnit: true,
main: props.unitType == 0, main: unitType == 0,
grid: props.unitType == 1, grid: unitType == 1,
friend: props.unitType == 2, friend: unitType == 2,
editable: props.editable, editable: editable,
filled: props.gridSummon !== undefined, filled: gridSummon !== undefined,
}) })
const gridSummon = props.gridSummon const buttonClasses = classNames({
Options: true,
Clicked: contextMenuOpen,
})
// Other
const summon = gridSummon?.object const summon = gridSummon?.object
// Hooks
useEffect(() => { useEffect(() => {
generateImageUrl() generateImageUrl()
}) })
// Methods: Open layer
function openSearchModal(event: MouseEvent<HTMLDivElement>) {
if (editable) setSearchModalOpen(true)
}
function openRemoveSummonAlert() {
setAlertOpen(true)
}
// Methods: Handle button clicked
function handleButtonClicked() {
setContextMenuOpen(!contextMenuOpen)
}
// Methods: Handle open change
function handleContextMenuOpenChange(open: boolean) {
if (!open) setContextMenuOpen(false)
}
function handleSearchModalOpenChange(open: boolean) {
setSearchModalOpen(open)
}
// Methods: Mutate data
function passUncapData(uncap: number) {
if (gridSummon) updateUncap(gridSummon.id, position, uncap)
}
function removeSummon() {
if (gridSummon) sendSummonToRemove(gridSummon.id)
}
// Methods: Image string generation
function generateImageUrl() { function generateImageUrl() {
let imgSrc = '' let imgSrc = ''
if (props.gridSummon) { if (gridSummon) {
const summon = props.gridSummon.object! const summon = gridSummon.object!
const upgradedSummons = [ const upgradedSummons = [
'2040094000', '2040094000',
@ -71,12 +135,12 @@ const SummonUnit = (props: Props) => {
let suffix = '' let suffix = ''
if ( if (
upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 && upgradedSummons.indexOf(summon.granblue_id.toString()) != -1 &&
props.gridSummon.uncap_level == 5 gridSummon.uncap_level == 5
) )
suffix = '_02' suffix = '_02'
// Generate the correct source for the summon // Generate the correct source for the summon
if (props.unitType == 0 || props.unitType == 2) if (unitType == 0 || unitType == 2)
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-main/${summon.granblue_id}${suffix}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-main/${summon.granblue_id}${suffix}.jpg`
else else
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg` imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/summon-grid/${summon.granblue_id}${suffix}.jpg`
@ -86,27 +150,80 @@ const SummonUnit = (props: Props) => {
} }
function placeholderImageUrl() { function placeholderImageUrl() {
return props.unitType == 0 || props.unitType == 2 return unitType == 0 || unitType == 2
? '/images/placeholders/placeholder-summon-main.png' ? '/images/placeholders/placeholder-summon-main.png'
: '/images/placeholders/placeholder-summon-grid.png' : '/images/placeholders/placeholder-summon-grid.png'
} }
function passUncapData(uncap: number) { // Methods: Layer element rendering
if (props.gridSummon) const contextMenu = () => {
props.updateUncap(props.gridSummon.id, props.position, uncap) if (editable && gridSummon && gridSummon.id) {
return (
<>
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
<ContextMenuTrigger asChild>
<Button
accessoryIcon={<SettingsIcon />}
className={buttonClasses}
onClick={handleButtonClicked}
/>
</ContextMenuTrigger>
<ContextMenuContent align="start">
<ContextMenuItem onSelect={openRemoveSummonAlert}>
{t('context.remove')}
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
{removeAlert()}
</>
)
}
} }
const removeAlert = () => {
return (
<Alert
open={alertOpen}
primaryAction={removeSummon}
primaryActionText={t('modals.summon.buttons.remove')}
cancelAction={() => setAlertOpen(false)}
cancelActionText={t('buttons.cancel')}
message={
<Trans i18nKey="modals.summons.messages.remove">
Are you sure you want to remove{' '}
<strong>{{ weapon: gridSummon?.object.name[locale] }}</strong> from
your team?
</Trans>
}
/>
)
}
const searchModal = () => {
return (
<SearchModal
placeholderText={t('search.placeholders.summon')}
fromPosition={position}
object="summons"
open={searchModalOpen}
onOpenChange={handleSearchModalOpenChange}
send={updateObject}
/>
)
}
// Methods: Core element rendering
const image = ( const image = (
<div className="SummonImage"> <div className="SummonImage" onClick={openSearchModal}>
<img <img
alt={summon?.name.en} alt={summon?.name[locale]}
className={classNames({ className={classNames({
GridImage: true, GridImage: true,
Placeholder: imageUrl === '', Placeholder: imageUrl === '',
})} })}
src={imageUrl !== '' ? imageUrl : placeholderImageUrl()} src={imageUrl !== '' ? imageUrl : placeholderImageUrl()}
/> />
{props.editable ? ( {editable ? (
<span className="icon"> <span className="icon">
<PlusIcon /> <PlusIcon />
</span> </span>
@ -116,20 +233,11 @@ const SummonUnit = (props: Props) => {
</div> </div>
) )
const editableImage = (
<SearchModal
placeholderText={t('search.placeholders.summon')}
fromPosition={props.position}
object="summons"
send={props.updateObject}
>
{image}
</SearchModal>
)
const unitContent = ( const unitContent = (
<>
<div className={classes}> <div className={classes}>
{props.editable ? editableImage : image} {contextMenu()}
{image}
{gridSummon ? ( {gridSummon ? (
<UncapIndicator <UncapIndicator
type="summon" type="summon"
@ -144,13 +252,15 @@ const SummonUnit = (props: Props) => {
)} )}
<h3 className="SummonName">{summon?.name[locale]}</h3> <h3 className="SummonName">{summon?.name[locale]}</h3>
</div> </div>
{searchModal()}
</>
) )
const withHovercard = ( const unitContentWithHovercard = (
<SummonHovercard gridSummon={gridSummon!}>{unitContent}</SummonHovercard> <SummonHovercard gridSummon={gridSummon!}>{unitContent}</SummonHovercard>
) )
return gridSummon && !props.editable ? withHovercard : unitContent return gridSummon && !editable ? unitContentWithHovercard : unitContent
} }
export default SummonUnit export default SummonUnit

View file

@ -520,7 +520,7 @@ const WeaponUnit = ({
</div> </div>
</div> </div>
<img <img
alt={weapon?.name.en} alt={weapon?.name[locale]}
className={classNames({ className={classNames({
GridImage: true, GridImage: true,
Placeholder: imageUrl === '', Placeholder: imageUrl === '',

View file

@ -250,6 +250,11 @@
"password_confirm": "Password (again)" "password_confirm": "Password (again)"
} }
}, },
"summon": {
"buttons": {
"remove": "Remove summon"
}
},
"weapon": { "weapon": {
"title": "Modify Weapon", "title": "Modify Weapon",
"buttons": { "buttons": {

View file

@ -251,6 +251,11 @@
"password_confirm": "パスワード確認" "password_confirm": "パスワード確認"
} }
}, },
"summon": {
"buttons": {
"remove": "召喚石を削除する"
}
},
"weapon": { "weapon": {
"title": "武器変更", "title": "武器変更",
"buttons": { "buttons": {

View file

@ -154,6 +154,7 @@ api.createEntity({ name: 'users' })
api.createEntity({ name: 'parties' }) api.createEntity({ name: 'parties' })
api.createEntity({ name: 'grid_characters' }) api.createEntity({ name: 'grid_characters' })
api.createEntity({ name: 'grid_weapons' }) api.createEntity({ name: 'grid_weapons' })
api.createEntity({ name: 'grid_summons' })
api.createEntity({ name: 'characters' }) api.createEntity({ name: 'characters' })
api.createEntity({ name: 'weapons' }) api.createEntity({ name: 'weapons' })
api.createEntity({ name: 'summons' }) api.createEntity({ name: 'summons' })