Refactor object grids to handle business logic instead of Party

This commit is contained in:
Justin Edmund 2022-02-02 16:54:14 -08:00
parent 44966fe8fe
commit 827473ee5a
16 changed files with 726 additions and 477 deletions

View file

@ -1,11 +1,18 @@
import React, { useState } from 'react'
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useCookies } from 'react-cookie'
import { useModal as useModal } from '~utils/useModal'
import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce'
import CharacterUnit from '~components/CharacterUnit'
import SearchModal from '~components/SearchModal'
import api from '~utils/api'
import './index.scss'
// GridType
export enum GridType {
Class,
Character,
@ -13,67 +20,185 @@ export enum GridType {
Summon
}
// Props
interface Props {
userId?: string
grid: GridArray<Character>
partyId?: string
characters: GridArray<GridCharacter>
editable: boolean
exists: boolean
onSelect: (type: GridType, character: Character, position: number) => void
createParty: () => Promise<AxiosResponse<any, any>>
pushHistory?: (path: string) => void
}
const CharacterGrid = (props: Props) => {
const { open, openModal, closeModal } = useModal()
const [searchPosition, setSearchPosition] = useState(0)
// Constants
const numCharacters: number = 5
function isCharacter(object: Character | Weapon | Summon): object is Character {
// There aren't really any unique fields here
return (object as Character).gender !== undefined
}
// Cookies
const [cookies, _] = useCookies(['user'])
const headers = (cookies.user != null) ? {
headers: {
'Authorization': `Bearer ${cookies.user.access_token}`
}
} : {}
// Set up states for Grid data
const [characters, setCharacters] = useState<GridArray<GridCharacter>>({})
// Set up states for Search
const { open, openModal, closeModal } = useModal()
const [itemPositionForSearch, setItemPositionForSearch] = useState(0)
// Create a temporary state to store previous character uncap values
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
// Create a state dictionary to store pure objects for Search
const [searchGrid, setSearchGrid] = useState<GridArray<Character>>({})
// Set states from props
useEffect(() => {
setCharacters(props.characters || {})
}, [props])
// Update search grid whenever characters are updated
useEffect(() => {
let newSearchGrid = Object.values(characters).map((o) => o.character)
setSearchGrid(newSearchGrid)
}, [characters])
// Methods: Adding an object from search
function openSearchModal(position: number) {
setSearchPosition(position)
setItemPositionForSearch(position)
openModal()
}
function receiveCharacter(character: Character, position: number) {
props.onSelect(GridType.Character, character, position)
}
function receiveCharacterFromSearch(object: Character | Weapon | Summon, position: number) {
const character = object as Character
function sendData(object: Character | Weapon | Summon, position: number) {
if (isCharacter(object)) {
receiveCharacter(object, position)
if (!props.partyId) {
props.createParty()
.then(response => {
const party = response.data.party
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveCharacter(party.id, character, position)
.then(response => storeGridCharacter(response.data.grid_character))
})
} else {
saveCharacter(props.partyId, character, position)
.then(response => storeGridCharacter(response.data.grid_character))
}
}
async function saveCharacter(partyId: string, character: Character, position: number) {
return await api.endpoints.characters.create({
'character': {
'party_id': partyId,
'character_id': character.id,
'position': position,
'mainhand': (position == -1),
'uncap_level': characterUncapLevel(character)
}
}, headers)
}
function storeGridCharacter(gridCharacter: GridCharacter) {
// Store the grid unit at the correct position
let newCharacters = Object.assign({}, characters)
newCharacters[gridCharacter.position] = gridCharacter
setCharacters(newCharacters)
}
// Methods: Helpers
function characterUncapLevel(character: Character) {
let uncapLevel
if (character.special) {
uncapLevel = 3
if (character.uncap.ulb) uncapLevel = 5
else if (character.uncap.flb) uncapLevel = 4
} else {
uncapLevel = 4
if (character.uncap.ulb) uncapLevel = 6
else if (character.uncap.flb) uncapLevel = 5
}
return uncapLevel
}
// Methods: Updating uncap level
// Note: Saves, but debouncing is not working properly
async function saveUncap(id: string, position: number, uncapLevel: number) {
try {
await api.updateUncap('weapon', id, uncapLevel)
.then(response => {
storeGridCharacter(response.data.grid_character)
})
} catch (error) {
console.error(error)
// Revert optimistic UI
updateUncapLevel(position, previousUncapValues[position])
// Remove optimistic key
let newPreviousValues = {...previousUncapValues}
delete newPreviousValues[position]
setPreviousUncapValues(newPreviousValues)
}
}
const initiateUncapUpdate = useCallback(
(id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel)
// Save the current value in case of an unexpected result
let newPreviousValues = {...previousUncapValues}
newPreviousValues[position] = characters[position].uncap_level
setPreviousUncapValues(newPreviousValues)
// Optimistically update UI
updateUncapLevel(position, uncapLevel)
}, [previousUncapValues, characters]
)
const debouncedAction = useMemo(() =>
debounce((id, position, number) => {
saveUncap(id, position, number)
}, 1000), [saveUncap]
)
const updateUncapLevel = (position: number, uncapLevel: number) => {
let newCharacters = Object.assign({}, characters)
newCharacters[position].uncap_level = uncapLevel
setCharacters(newCharacters)
}
// Render: JSX components
return (
<div className="CharacterGrid">
<ul id="grid_characters">
{
Array.from(Array(numCharacters)).map((x, i) => {
return (
<li key={`grid_unit_${i}`} >
<CharacterUnit
onClick={() => { openSearchModal(i) }}
editable={props.editable}
position={i}
character={props.grid[i]}
/>
</li>
)
})
}
{Array.from(Array(numCharacters)).map((x, i) => {
return (
<li key={`grid_unit_${i}`} >
<CharacterUnit
gridCharacter={props.characters[i]}
editable={props.editable}
position={i}
onClick={() => { openSearchModal(i) }}
updateUncap={initiateUncapUpdate}
/>
</li>
)
})}
{open ? (
<SearchModal
grid={props.grid}
close={closeModal}
send={sendData}
fromPosition={searchPosition}
object="characters"
placeholderText="Search for a character..."
/>
) : null}
<SearchModal
grid={searchGrid}
close={closeModal}
send={receiveCharacterFromSearch}
fromPosition={itemPositionForSearch}
object="characters"
placeholderText="Search for a character..."
/>
) : null}
</ul>
</div>
)

View file

@ -2,45 +2,53 @@ import React, { useEffect, useState } from 'react'
import classnames from 'classnames'
import UncapIndicator from '~components/UncapIndicator'
import PlusIcon from '~public/icons/plus.svg'
import './index.scss'
interface Props {
onClick: () => void
character: Character | undefined
gridCharacter: GridCharacter | undefined
position: number
editable: boolean
onClick: () => void
updateUncap: (id: string, position: number, uncap: number) => void
}
const CharacterUnit = (props: Props) => {
console.log(props.gridCharacter?.character.name.en, props.gridCharacter?.uncap_level)
const [imageUrl, setImageUrl] = useState('')
const classes = classnames({
CharacterUnit: true,
'editable': props.editable,
'filled': (props.character !== undefined)
'filled': (props.gridCharacter !== undefined)
})
const character = props.character
const gridCharacter = props.gridCharacter
const character = gridCharacter?.character
useEffect(() => {
generateImageUrl()
})
function generateImageUrl() {
let imgSrc = ""
if (props.character) {
const character = props.character!
if (props.gridCharacter) {
const character = props.gridCharacter.character!
imgSrc = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/chara-main/${character.granblue_id}_01.jpg`
}
setImageUrl(imgSrc)
}
function passUncapData(uncap: number) {
console.log(`passuncapdata ${uncap}`)
if (props.gridCharacter)
props.updateUncap(props.gridCharacter.id, props.position, uncap)
}
return (
<div>
<div className={classes}>
@ -48,11 +56,15 @@ const CharacterUnit = (props: Props) => {
<img alt={character?.name.en} className="grid_image" src={imageUrl} />
{ (props.editable) ? <span className='icon'><PlusIcon /></span> : '' }
</div>
<UncapIndicator
type="character"
flb={character?.uncap.flb || false}
uncapLevel={(character?.rarity == 2) ? 3 : 4}
/>
{ (gridCharacter && character) ?
<UncapIndicator
type="character"
flb={character.uncap.flb || false}
ulb={character.uncap.ulb || false}
uncapLevel={gridCharacter.uncap_level}
updateUncap={passUncapData}
special={character.special}
/> : '' }
<h3 className="CharacterName">{character?.name.en}</h3>
</div>
</div>

View file

@ -18,7 +18,7 @@ interface Props {
found?: boolean
offset: number
onClick: (position: number) => void
updateUncap: (id: string, uncap: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
}
const ExtraSummons = (props: Props) => {

View file

@ -15,11 +15,10 @@ export enum GridType {
interface Props {
grid: GridArray<GridWeapon>
editable: boolean
exists: boolean
found?: boolean
offset: number
onClick: (position: number) => void
updateUncap: (id: string, uncap: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
}
const ExtraWeapons = (props: Props) => {

View file

@ -1,15 +1,14 @@
import React, { ChangeEvent, useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useCookies } from 'react-cookie'
import api from '~utils/api'
// UI Elements
import PartySegmentedControl from '~components/PartySegmentedControl'
// Grids
import WeaponGrid from '~components/WeaponGrid'
import SummonGrid from '~components/SummonGrid'
import CharacterGrid from '~components/CharacterGrid'
import api from '~utils/api'
import './index.scss'
// GridType
enum GridType {
Class,
@ -17,96 +16,58 @@ enum GridType {
Weapon,
Summon
}
export { GridType }
import './index.scss'
// Props
interface Props {
partyId?: string
mainWeapon?: GridWeapon
mainSummon?: GridSummon
friendSummon?: GridSummon
characters?: GridArray<Character>
characters?: GridArray<GridCharacter>
weapons?: GridArray<GridWeapon>
summons?: GridArray<GridSummon>
extra: boolean
editable: boolean
exists: boolean
pushHistory?: (path: string) => void
}
const Party = (props: Props) => {
// Cookies
const [cookies, _] = useCookies(['user'])
const headers = (cookies.user != null) ? {
headers: {
'Authorization': `Bearer ${cookies.user.access_token}`
}
} : {}
// Grid data
const [characters, setCharacters] = useState<GridArray<Character>>({})
const [weapons, setWeapons] = useState<GridArray<GridWeapon>>({})
const [summons, setSummons] = useState<GridArray<GridSummon>>({})
const [mainWeapon, setMainWeapon] = useState<GridWeapon>()
const [mainSummon, setMainSummon] = useState<GridSummon>()
const [friendSummon, setFriendSummon] = useState<GridSummon>()
// Set up states
const [extra, setExtra] = useState<boolean>(false)
useEffect(() => {
setPartyId(props.partyId || '')
setMainWeapon(props.mainWeapon)
setMainSummon(props.mainSummon)
setFriendSummon(props.friendSummon)
setCharacters(props.characters || {})
setWeapons(props.weapons || {})
setSummons(props.summons || {})
setExtra(props.extra || false)
}, [props.partyId, props.mainWeapon, props.mainSummon, props.friendSummon, props.characters, props.weapons, props.summons, props.extra])
const weaponGrid = (
<WeaponGrid
userId={cookies.user ? cookies.user.userId : ''}
mainhand={mainWeapon}
grid={weapons}
editable={props.editable}
exists={props.exists}
extra={extra}
onSelect={itemSelected}
/>
)
const summonGrid = (
<SummonGrid
userId={cookies.user ? cookies.user.userId : ''}
main={mainSummon}
friend={friendSummon}
grid={summons}
editable={props.editable}
exists={props.exists}
onSelect={itemSelected}
/>
)
const characterGrid = (
<CharacterGrid
userId={cookies.user ? cookies.user.userId : ''}
grid={characters}
editable={props.editable}
exists={props.exists}
onSelect={itemSelected}
/>
)
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon)
const [partyId, setPartyId] = useState('')
// Set states from props
useEffect(() => {
setExtra(props.extra || false)
}, [props])
// Methods: Creating a new party
async function createParty() {
let body = {
party: {
...(cookies.user) && { user_id: cookies.user.user_id },
is_extra: extra
}
}
return await api.endpoints.parties.create(body, headers)
}
// Methods: Updating the party's extra flag
// Note: This doesn't save to the server yet.
function checkboxChanged(event: React.ChangeEvent<HTMLInputElement>) {
setExtra(event.target.checked)
}
// Methods: Navigating with segmented control
function segmentClicked(event: React.ChangeEvent<HTMLInputElement>) {
switch(event.target.value) {
case 'class':
@ -126,174 +87,66 @@ const Party = (props: Props) => {
}
}
function itemSelected(type: GridType, item: Character | Weapon | Summon, position: number) {
if (!partyId) {
createParty()
.then(response => {
return response.data.party
})
.then(party => {
if (props.pushHistory) {
props.pushHistory(`/p/${party.shortcode}`)
}
// Render: JSX components
const navigation = (
<PartySegmentedControl
extra={props.extra}
editable={props.editable}
selectedTab={currentTab}
onClick={segmentClicked}
onCheckboxChange={checkboxChanged}
/>
)
return party.id
})
.then(partyId => {
setPartyId(partyId)
saveItem(partyId, type, item, position)
})
} else {
saveItem(partyId, type, item, position)
}
}
const weaponGrid = (
<WeaponGrid
partyId={props.partyId}
mainhand={props.mainWeapon}
weapons={props.weapons || {}}
extra={props.extra}
editable={props.editable}
createParty={createParty}
pushHistory={props.pushHistory}
/>
)
async function createParty() {
const body = (!cookies.user) ? {
party: {
is_extra: extra
}
} : {
party: {
user_id: cookies.user.userId,
is_extra: extra
}
}
const summonGrid = (
<SummonGrid
partyId={props.partyId}
mainSummon={props.mainSummon}
friendSummon={props.friendSummon}
summons={props.summons || {}}
editable={props.editable}
createParty={createParty}
pushHistory={props.pushHistory}
/>
)
return await api.endpoints.parties.create(body, headers)
}
const characterGrid = (
<CharacterGrid
partyId={props.partyId}
characters={props.characters || {}}
editable={props.editable}
createParty={createParty}
pushHistory={props.pushHistory}
/>
)
function saveItem(partyId: string, type: GridType, item: Character | Weapon | Summon, position: number) {
switch(type) {
case GridType.Class:
saveClass()
break
const currentGrid = () => {
switch(currentTab) {
case GridType.Character:
const character = item as Character
saveCharacter(character, position, partyId)
.then(() => {
storeCharacter(character, position)
})
break
return characterGrid
case GridType.Weapon:
const weapon = item as Weapon
saveWeapon(weapon, position, partyId)
.then((response) => {
storeWeapon(response.data.grid_weapon)
})
break
return weaponGrid
case GridType.Summon:
const summon = item as Summon
saveSummon(summon, position, partyId)
.then((response) => {
storeSummon(response.data.grid_summon, position)
})
break
return summonGrid
}
}
// Weapons
function storeWeapon(weapon: GridWeapon) {
if (weapon.position == -1) {
setMainWeapon(weapon)
} else {
// Store the grid unit weapon at the correct position
let newWeapons = Object.assign({}, weapons)
newWeapons[weapon.position!] = weapon
setWeapons(newWeapons)
}
}
async function saveWeapon(weapon: Weapon, position: number, party: string) {
let uncapLevel = 3
if (weapon.uncap.ulb)
uncapLevel = 5
else if (weapon.uncap.flb)
uncapLevel = 4
return await api.endpoints.weapons.create({
'weapon': {
'party_id': party,
'weapon_id': weapon.id,
'position': position,
'mainhand': (position == -1),
'uncap_level': uncapLevel
}
}, headers)
}
// Summons
function storeSummon(summon: GridSummon, position: number) {
if (position == -1) {
setMainSummon(summon)
} else if (position == 6) {
setFriendSummon(summon)
} else {
// Store the grid unit summon at the correct position
let newSummons = Object.assign({}, summons)
newSummons[position] = summon
setSummons(newSummons)
}
}
async function saveSummon(summon: Summon, position: number, party: string) {
return await api.endpoints.summons.create({
'summon': {
'party_id': party,
'summon_id': summon.id,
'position': position,
'main': (position == -1),
'friend': (position == 6)
}
}, headers)
}
// Character
function storeCharacter(character: Character, position: number) {
// Store the grid unit character at the correct position
let newCharacters = Object.assign({}, characters)
newCharacters[position] = character
setCharacters(newCharacters)
}
async function saveCharacter(character: Character, position: number, party: string) {
await api.endpoints.characters.create({
'character': {
'party_id': party,
'character_id': character.id,
'position': position
}
}, headers)
}
// Class
function saveClass() {
// TODO: Implement this
}
return (
<div>
<PartySegmentedControl
extra={extra}
editable={props.editable}
selectedTab={currentTab}
onClick={segmentClicked}
onCheckboxChange={checkboxChanged}
/>
{
(() => {
switch(currentTab) {
case GridType.Character:
return characterGrid
case GridType.Weapon:
return weaponGrid
case GridType.Summon:
return summonGrid
}
})()
}
{ navigation }
{ currentGrid() }
</div>
)
}

View file

@ -1,6 +1,9 @@
import React, { useCallback, useState } from 'react'
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useCookies } from 'react-cookie'
import { useModal as useModal } from '~utils/useModal'
import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce'
import SearchModal from '~components/SearchModal'
@ -20,128 +23,237 @@ export enum GridType {
// Props
interface Props {
userId?: string
partyId?: string
main?: GridSummon | undefined
friend?: GridSummon | undefined
grid: GridArray<GridSummon>
mainSummon: GridSummon | undefined
friendSummon: GridSummon | undefined
summons: GridArray<GridSummon>
editable: boolean
exists: boolean
found?: boolean
onSelect: (type: GridType, summon: Summon, position: number) => void
createParty: () => Promise<AxiosResponse<any, any>>
pushHistory?: (path: string) => void
}
const SummonGrid = (props: Props) => {
const { open, openModal, closeModal } = useModal()
const [searchPosition, setSearchPosition] = useState(0)
// Constants
const numSummons: number = 4
const searchGrid: GridArray<Summon> = Object.values(props.grid).map((o) => o.summon)
function receiveSummon(summon: Summon, position: number) {
props.onSelect(GridType.Summon, summon, position)
}
function sendData(object: Character | Weapon | Summon, position: number) {
if (isSummon(object)) {
receiveSummon(object, position)
// Cookies
const [cookies, _] = useCookies(['user'])
const headers = (cookies.user != null) ? {
headers: {
'Authorization': `Bearer ${cookies.user.access_token}`
}
}
} : {}
function isSummon(object: Character | Weapon | Summon): object is Summon {
// There aren't really any unique fields here
return (object as Summon).granblue_id !== undefined
}
// Set up states for Grid data
const [summons, setSummons] = useState<GridArray<GridSummon>>({})
const [mainSummon, setMainSummon] = useState<GridSummon>()
const [friendSummon, setFriendSummon] = useState<GridSummon>()
// Set up states for Search
const { open, openModal, closeModal } = useModal()
const [itemPositionForSearch, setItemPositionForSearch] = useState(0)
// Create a temporary state to store previous weapon uncap value
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
// Create a state dictionary to store pure objects for Search
const [searchGrid, setSearchGrid] = useState<GridArray<Summon>>({})
// Set states from props
useEffect(() => {
setSummons(props.summons || {})
setMainSummon(props.mainSummon)
setFriendSummon(props.friendSummon)
}, [props])
// Update search grid whenever any summon is updated
useEffect(() => {
let newSearchGrid = Object.values(summons).map((o) => o.summon)
if (mainSummon)
newSearchGrid.unshift(mainSummon.summon)
if (friendSummon)
newSearchGrid.unshift(friendSummon.summon)
setSearchGrid(newSearchGrid)
}, [summons, mainSummon, friendSummon])
// Methods: Adding an object from search
function openSearchModal(position: number) {
setSearchPosition(position)
setItemPositionForSearch(position)
openModal()
}
async function updateUncap(id: string, level: number) {
await api.updateUncap('summon', id, level)
.catch(error => {
console.error(error)
})
function receiveSummonFromSearch(object: Character | Weapon | Summon, position: number) {
const summon = object as Summon
if (!props.partyId) {
props.createParty()
.then(response => {
const party = response.data.party
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveSummon(party.id, summon, position)
.then(response => storeGridSummon(response.data.grid_summon))
})
} else {
saveSummon(props.partyId, summon, position)
.then(response => storeGridSummon(response.data.grid_summon))
}
}
const initiateUncapUpdate = (id: string, uncapLevel: number) => {
debouncedAction(id, uncapLevel)
async function saveSummon(partyId: string, summon: Summon, position: number) {
let uncapLevel = 3
if (summon.uncap.ulb) uncapLevel = 5
else if (summon.uncap.flb) uncapLevel = 4
return await api.endpoints.summons.create({
'summon': {
'party_id': partyId,
'summon_id': summon.id,
'position': position,
'main': (position == -1),
'friend': (position == 6),
'uncap_level': uncapLevel
}
}, headers)
}
const debouncedAction = useCallback(
() => debounce((id, number) => {
updateUncap(id, number)
}, 1000), []
)()
function storeGridSummon(gridSummon: GridSummon) {
if (gridSummon.position == -1) {
setMainSummon(gridSummon)
} else if (gridSummon.position == 6) {
setFriendSummon(gridSummon)
} else {
// Store the grid unit at the correct position
let newSummons = Object.assign({}, summons)
newSummons[gridSummon.position] = gridSummon
setSummons(newSummons)
}
}
// Methods: Updating uncap level
// Note: Saves, but debouncing is not working properly
async function saveUncap(id: string, position: number, uncapLevel: number) {
try {
await api.updateUncap('summon', id, uncapLevel)
.then(response => {
storeGridSummon(response.data.grid_summon)
})
} catch (error) {
console.error(error)
// Revert optimistic UI
updateUncapLevel(position, previousUncapValues[position])
// Remove optimistic key
let newPreviousValues = {...previousUncapValues}
delete newPreviousValues[position]
setPreviousUncapValues(newPreviousValues)
}
}
const initiateUncapUpdate = useCallback(
(id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel)
// Save the current value in case of an unexpected result
let newPreviousValues = {...previousUncapValues}
newPreviousValues[position] = summons[position].uncap_level
setPreviousUncapValues(newPreviousValues)
// Optimistically update UI
updateUncapLevel(position, uncapLevel)
}, [previousUncapValues, summons]
)
const debouncedAction = useMemo(() =>
debounce((id, position, number) => {
saveUncap(id, position, number)
}, 1000), [saveUncap]
)
const updateUncapLevel = (position: number, uncapLevel: number) => {
let newSummons = Object.assign({}, summons)
newSummons[position].uncap_level = uncapLevel
setSummons(newSummons)
}
// Render: JSX components
const mainSummonElement = (
<div className="LabeledUnit">
<div className="Label">Main Summon</div>
<SummonUnit
gridSummon={props.mainSummon}
editable={props.editable}
key="grid_main_summon"
position={-1}
unitType={0}
onClick={() => { openSearchModal(-1) }}
updateUncap={initiateUncapUpdate}
/>
</div>
)
const friendSummonElement = (
<div className="LabeledUnit">
<div className="Label">Friend Summon</div>
<SummonUnit
gridSummon={props.friendSummon}
editable={props.editable}
key="grid_friend_summon"
position={6}
unitType={2}
onClick={() => { openSearchModal(6) }}
updateUncap={initiateUncapUpdate}
/>
</div>
)
const summonGridElement = (
<div id="LabeledGrid">
<div className="Label">Summons</div>
<ul id="grid_summons">
{Array.from(Array(numSummons)).map((x, i) => {
return (<li key={`grid_unit_${i}`} >
<SummonUnit
gridSummon={props.summons[i]}
editable={props.editable}
position={i}
unitType={1}
onClick={() => { openSearchModal(i) }}
updateUncap={initiateUncapUpdate}
/>
</li>)
})}
</ul>
</div>
)
const subAuraSummonElement = (
<ExtraSummons
grid={props.summons}
editable={props.editable}
exists={false}
offset={numSummons}
onClick={openSearchModal}
updateUncap={initiateUncapUpdate}
/>
)
return (
<div>
<div className="SummonGrid">
<div className="LabeledUnit">
<div className="Label">Main Summon</div>
<SummonUnit
editable={props.editable}
key="grid_main_summon"
position={-1}
unitType={0}
gridSummon={props.main}
onClick={() => { openSearchModal(-1) }}
updateUncap={initiateUncapUpdate}
/>
</div>
<div className="LabeledUnit">
<div className="Label">Friend Summon</div>
<SummonUnit
editable={props.editable}
key="grid_friend_summon"
position={6}
unitType={2}
gridSummon={props.friend}
onClick={() => { openSearchModal(6) }}
updateUncap={initiateUncapUpdate}
/>
</div>
<div id="LabeledGrid">
<div className="Label">Summons</div>
<ul id="grid_summons">
{
Array.from(Array(numSummons)).map((x, i) => {
return (
<li key={`grid_unit_${i}`} >
<SummonUnit
editable={props.editable}
position={i}
unitType={1}
gridSummon={props.grid[i]}
onClick={() => { openSearchModal(i) }}
updateUncap={initiateUncapUpdate}
/>
</li>
)
})
}
</ul>
</div>
{ mainSummonElement }
{ friendSummonElement }
{ summonGridElement }
</div>
<ExtraSummons
grid={props.grid}
editable={props.editable}
exists={false}
offset={numSummons}
onClick={openSearchModal}
updateUncap={initiateUncapUpdate}
/>
{ subAuraSummonElement }
{open ? (
<SearchModal
grid={searchGrid}
close={closeModal}
send={sendData}
fromPosition={searchPosition}
send={receiveSummonFromSearch}
fromPosition={itemPositionForSearch}
object="summons"
placeholderText="Search for a summon..."
/>

View file

@ -8,7 +8,7 @@ import './index.scss'
interface Props {
onClick: () => void
updateUncap: (id: string, uncap: number) => void
updateUncap: (id: string, position: number, uncap: number) => void
gridSummon: GridSummon | undefined
position: number
editable: boolean
@ -67,7 +67,8 @@ const SummonUnit = (props: Props) => {
ulb={summon?.uncap.ulb || false}
flb={summon?.uncap.flb || false}
uncapLevel={gridSummon?.uncap_level}
updateUncap={passUncapData}
updateUncap={passUncapData}
special={false}
/> : '' }
<h3 className="SummonName">{summon?.name.en}</h3>
</div>

View file

@ -8,27 +8,36 @@ interface Props {
rarity?: number
uncapLevel: number
flb: boolean
ulb?: boolean
ulb: boolean
special: boolean
updateUncap: (uncap: number) => void
}
const UncapIndicator = (props: Props) => {
const [uncap, setUncap] = useState(props.uncapLevel)
useEffect(() => {
props.updateUncap(uncap)
}, [uncap])
const numStars = setNumStars()
function setNumStars() {
let numStars
if (props.type === 'character') {
if (props.flb) {
numStars = 5
if (props.special) {
if (props.ulb) {
numStars = 5
} else if (props.flb) {
numStars = 4
} else {
numStars = 3
}
} else {
numStars = 4
}
if (props.ulb) {
numStars = 6
} else if (props.flb) {
numStars = 5
} else {
numStars = 4
}
}
} else {
if (props.ulb) {
numStars = 5
@ -43,31 +52,38 @@ const UncapIndicator = (props: Props) => {
}
function toggleStar(index: number, empty: boolean) {
if (empty) setUncap(index + 1)
else setUncap(index)
if (empty) props.updateUncap(index + 1)
else props.updateUncap(index)
}
const transcendence = (i: number) => {
return <UncapStar ulb={true} empty={i >= uncap} key={`star_${i}`} index={i} onClick={toggleStar} />
return <UncapStar ulb={true} empty={i >= props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} />
}
const ulb = (i: number) => {
return <UncapStar ulb={true} empty={i >= props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} />
}
const flb = (i: number) => {
return <UncapStar flb={true} empty={i >= uncap} key={`star_${i}`} index={i} onClick={toggleStar} />
return <UncapStar flb={true} empty={i >= props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} />
}
const mlb = (i: number) => {
return <UncapStar empty={i >= uncap} key={`star_${i}`} index={i} onClick={toggleStar} />
// console.log("MLB; Number of stars:", props.uncapLevel)
return <UncapStar empty={i >= props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} />
}
return (
<ul className="UncapIndicator">
{
Array.from(Array(numStars)).map((x, i) => {
if (props.type === 'character' && i > 4) {
return transcendence(i)
if (props.special)
return ulb(i)
else
return transcendence(i)
} else if (
props.special && props.type === 'character' && i == 3 ||
props.type === 'character' && i == 4 ||
props.type !== 'character' && i > 2) {
return flb(i)

View file

@ -1,6 +1,9 @@
import React, { useCallback, useState } from 'react'
/* eslint-disable react-hooks/exhaustive-deps */
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { useCookies } from 'react-cookie'
import { useModal as useModal } from '~utils/useModal'
import { AxiosResponse } from 'axios'
import debounce from 'lodash.debounce'
import SearchModal from '~components/SearchModal'
@ -20,65 +23,199 @@ export enum GridType {
// Props
interface Props {
userId?: string
partyId?: string
mainhand?: GridWeapon | undefined
grid: GridArray<GridWeapon>
mainhand: GridWeapon | undefined
weapons: GridArray<GridWeapon>
extra: boolean
editable: boolean
exists: boolean
found?: boolean
onSelect: (type: GridType, weapon: Weapon, position: number) => void
createParty: () => Promise<AxiosResponse<any, any>>
pushHistory?: (path: string) => void
}
const WeaponGrid = (props: Props) => {
const { open, openModal, closeModal } = useModal()
const [searchPosition, setSearchPosition] = useState(0)
// Constants
const numWeapons: number = 9
const searchGrid: GridArray<Weapon> = Object.values(props.grid).map((o) => o.weapon)
function receiveWeapon(weapon: Weapon, position: number) {
props.onSelect(GridType.Weapon, weapon, position)
}
function sendData(object: Character | Weapon | Summon, position: number) {
if (isWeapon(object)) {
receiveWeapon(object, position)
// Cookies
const [cookies, _] = useCookies(['user'])
const headers = (cookies.user != null) ? {
headers: {
'Authorization': `Bearer ${cookies.user.access_token}`
}
}
} : {}
function isWeapon(object: Character | Weapon | Summon): object is Weapon {
return (object as Weapon).proficiency !== undefined
}
// Set up states for Grid data
const [weapons, setWeapons] = useState<GridArray<GridWeapon>>({})
const [mainWeapon, setMainWeapon] = useState<GridWeapon>()
// Set up states for Search
const { open, openModal, closeModal } = useModal()
const [itemPositionForSearch, setItemPositionForSearch] = useState(0)
// Create a temporary state to store previous weapon uncap values
const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({})
// Create a state dictionary to store pure objects for Search
const [searchGrid, setSearchGrid] = useState<GridArray<Weapon>>({})
// Set states from props
useEffect(() => {
setWeapons(props.weapons || {})
setMainWeapon(props.mainhand)
}, [props])
// Update search grid whenever weapons or the mainhand are updated
useEffect(() => {
let newSearchGrid = Object.values(weapons).map((o) => o.weapon)
if (mainWeapon)
newSearchGrid.unshift(mainWeapon.weapon)
setSearchGrid(newSearchGrid)
}, [weapons, mainWeapon])
// Methods: Adding an object from search
function openSearchModal(position: number) {
setSearchPosition(position)
setItemPositionForSearch(position)
openModal()
}
async function updateUncap(id: string, level: number) {
await api.updateUncap('weapon', id, level)
.catch(error => {
console.error(error)
})
function receiveWeaponFromSearch(object: Character | Weapon | Summon, position: number) {
const weapon = object as Weapon
if (!props.partyId) {
props.createParty()
.then(response => {
const party = response.data.party
if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`)
saveWeapon(party.id, weapon, position)
.then(response => storeGridWeapon(response.data.grid_weapon))
})
} else {
saveWeapon(props.partyId, weapon, position)
.then(response => storeGridWeapon(response.data.grid_weapon))
}
}
const initiateUncapUpdate = (id: string, uncapLevel: number) => {
debouncedAction(id, uncapLevel)
async function saveWeapon(partyId: string, weapon: Weapon, position: number) {
let uncapLevel = 3
if (weapon.uncap.ulb) uncapLevel = 5
else if (weapon.uncap.flb) uncapLevel = 4
return await api.endpoints.weapons.create({
'weapon': {
'party_id': partyId,
'weapon_id': weapon.id,
'position': position,
'mainhand': (position == -1),
'uncap_level': uncapLevel
}
}, headers)
}
const debouncedAction = useCallback(
() => debounce((id, number) => {
updateUncap(id, number)
}, 1000), []
)()
function storeGridWeapon(gridWeapon: GridWeapon) {
if (gridWeapon.position == -1) {
setMainWeapon(gridWeapon)
} else {
// Store the grid unit at the correct position
let newWeapons = Object.assign({}, props.weapons)
newWeapons[gridWeapon.position] = gridWeapon
setWeapons(newWeapons)
}
}
const extraGrid = (
// Methods: Updating uncap level
// Note: Saves, but debouncing is not working properly
async function saveUncap(id: string, position: number, uncapLevel: number) {
// TODO: Don't make an API call if the new uncapLevel is the same as the current uncapLevel
try {
await api.updateUncap('weapon', id, uncapLevel)
.then(response => {
storeGridWeapon(response.data.grid_weapon)
})
} catch (error) {
console.error(error)
// Revert optimistic UI
updateUncapLevel(position, previousUncapValues[position])
// Remove optimistic key
let newPreviousValues = {...previousUncapValues}
delete newPreviousValues[position]
setPreviousUncapValues(newPreviousValues)
}
}
const memoizeAction = useCallback(
(id: string, position: number, uncapLevel: number) => {
debouncedAction(id, position, uncapLevel)
}, []
)
function initiateUncapUpdate(id: string, position: number, uncapLevel: number) {
memoizeAction(id, position, uncapLevel)
// Save the current value in case of an unexpected result
let newPreviousValues = {...previousUncapValues}
newPreviousValues[position] = (mainWeapon && position == -1) ? mainWeapon.uncap_level : weapons[position].uncap_level
setPreviousUncapValues(newPreviousValues)
// Optimistically update UI
updateUncapLevel(position, uncapLevel)
}
const debouncedAction = useMemo(() =>
debounce((id, position, number) => {
saveUncap(id, position, number)
}, 1000), [saveUncap]
)
const updateUncapLevel = (position: number, uncapLevel: number) => {
if (mainWeapon && position == -1) {
mainWeapon.uncap_level = uncapLevel
setMainWeapon(mainWeapon)
} else {
let newWeapons = Object.assign({}, weapons)
newWeapons[position].uncap_level = uncapLevel
setWeapons(newWeapons)
}
}
// Render: JSX components
const mainhandElement = (
<WeaponUnit
gridWeapon={mainWeapon}
editable={props.editable}
key="grid_mainhand"
position={-1}
unitType={0}
onClick={() => { openSearchModal(-1) }}
updateUncap={initiateUncapUpdate}
/>
)
const weaponGridElement = (
Array.from(Array(numWeapons)).map((x, i) => {
return (
<li key={`grid_unit_${i}`} >
<WeaponUnit
gridWeapon={weapons[i]}
editable={props.editable}
position={i}
unitType={1}
onClick={() => { openSearchModal(i) }}
updateUncap={initiateUncapUpdate}
/>
</li>
)
})
)
const extraGridElement = (
<ExtraWeapons
grid={props.grid}
grid={weapons}
editable={props.editable}
exists={false}
offset={numWeapons}
onClick={openSearchModal}
updateUncap={initiateUncapUpdate}
@ -88,48 +225,18 @@ const WeaponGrid = (props: Props) => {
return (
<div id="weapon_grids">
<div id="WeaponGrid">
<WeaponUnit
editable={props.editable}
key="grid_mainhand"
position={-1}
unitType={0}
gridWeapon={props.mainhand}
onClick={() => { openSearchModal(-1) }}
updateUncap={initiateUncapUpdate}
/>
<ul id="grid_weapons">
{
Array.from(Array(numWeapons)).map((x, i) => {
return (
<li key={`grid_unit_${i}`} >
<WeaponUnit
editable={props.editable}
position={i}
unitType={1}
gridWeapon={props.grid[i]}
onClick={() => { openSearchModal(i) }}
updateUncap={initiateUncapUpdate}
/>
</li>
)
})
}
</ul>
{ mainhandElement }
<ul id="grid_weapons">{ weaponGridElement }</ul>
</div>
{ (() => {
if(props.extra) {
return extraGrid
}
})() }
{ (() => { return (props.extra) ? extraGridElement : '' })() }
{open ? (
<SearchModal
grid={searchGrid}
close={closeModal}
send={sendData}
fromPosition={searchPosition}
send={receiveWeaponFromSearch}
fromPosition={itemPositionForSearch}
object="weapons"
placeholderText="Search for a weapon..."
/>

View file

@ -7,12 +7,12 @@ import PlusIcon from '~public/icons/plus.svg'
import './index.scss'
interface Props {
onClick: () => void
updateUncap: (id: string, uncap: number) => void
gridWeapon: GridWeapon | undefined
unitType: 0 | 1
position: number
editable: boolean
unitType: 0 | 1
onClick: () => void
updateUncap: (id: string, position: number, uncap: number) => void
}
const WeaponUnit = (props: Props) => {
@ -48,8 +48,9 @@ const WeaponUnit = (props: Props) => {
}
function passUncapData(uncap: number) {
console.log("Passing uncap data to updateUncap callback...")
if (props.gridWeapon)
props.updateUncap(props.gridWeapon.id, uncap)
props.updateUncap(props.gridWeapon.id, props.position, uncap)
}
return (
@ -66,7 +67,8 @@ const WeaponUnit = (props: Props) => {
ulb={gridWeapon.weapon.uncap.ulb || false}
flb={gridWeapon.weapon.uncap.flb || false}
uncapLevel={gridWeapon.uncap_level}
updateUncap={passUncapData}
updateUncap={passUncapData}
special={false}
/> : ''
}
</div>

18
package-lock.json generated
View file

@ -12,6 +12,7 @@
"@radix-ui/react-label": "^0.1.4",
"@radix-ui/react-switch": "^0.1.4",
"@svgr/webpack": "^6.2.0",
"@types/axios": "^0.14.0",
"axios": "^0.25.0",
"classnames": "^2.3.1",
"lodash.debounce": "^4.0.8",
@ -2982,6 +2983,15 @@
"node": ">=10.13.0"
}
},
"node_modules/@types/axios": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
"integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=",
"deprecated": "This is a stub types definition for axios (https://github.com/mzabriskie/axios). axios provides its own type definitions, so you don't need @types/axios installed!",
"dependencies": {
"axios": "*"
}
},
"node_modules/@types/cookie": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",
@ -8754,6 +8764,14 @@
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="
},
"@types/axios": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz",
"integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=",
"requires": {
"axios": "*"
}
},
"@types/cookie": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.3.3.tgz",

View file

@ -17,6 +17,7 @@
"@radix-ui/react-label": "^0.1.4",
"@radix-ui/react-switch": "^0.1.4",
"@svgr/webpack": "^6.2.0",
"@types/axios": "^0.14.0",
"axios": "^0.25.0",
"classnames": "^2.3.1",
"lodash.debounce": "^4.0.8",

View file

@ -22,7 +22,7 @@ const PartyRoute: React.FC = () => {
const [loading, setLoading] = useState(true)
const [editable, setEditable] = useState(false)
const [characters, setCharacters] = useState<GridArray<Character>>({})
const [characters, setCharacters] = useState<GridArray<GridCharacter>>({})
const [weapons, setWeapons] = useState<GridArray<GridWeapon>>({})
const [summons, setSummons] = useState<GridArray<GridSummon>>({})
@ -73,11 +73,11 @@ const PartyRoute: React.FC = () => {
}
function populateCharacters(list: [GridCharacter]) {
let characters: GridArray<Character> = {}
let characters: GridArray<GridCharacter> = {}
list.forEach((object: GridCharacter) => {
if (object.position != null)
characters[object.position] = object.character
characters[object.position] = object
})
return characters

View file

@ -21,6 +21,7 @@ interface Character {
}
uncap: {
flb: boolean
ulb: boolean
}
race: {
race1: number
@ -31,4 +32,5 @@ interface Character {
proficiency2: number
}
position?: number
special: boolean
}

View file

@ -1,5 +1,6 @@
interface GridCharacter {
id: string
position: number | null
position: number
character: Character
uncap_level: number
}

View file

@ -2,7 +2,7 @@ interface GridSummon {
id: string
main: boolean
friend: boolean
position: number | null
position: number
summon: Summon
uncap_level: number
}