From 827473ee5a41dd138ad1c5f2db53b5a5c23b29dd Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 2 Feb 2022 16:54:14 -0800 Subject: [PATCH] Refactor object grids to handle business logic instead of Party --- components/CharacterGrid/index.tsx | 209 +++++++++++++++---- components/CharacterUnit/index.tsx | 38 ++-- components/ExtraSummons/index.tsx | 2 +- components/ExtraWeapons/index.tsx | 3 +- components/Party/index.tsx | 299 +++++++-------------------- components/SummonGrid/index.tsx | 300 +++++++++++++++++++--------- components/SummonUnit/index.tsx | 5 +- components/UncapIndicator/index.tsx | 50 +++-- components/WeaponGrid/index.tsx | 253 ++++++++++++++++------- components/WeaponUnit/index.tsx | 12 +- package-lock.json | 18 ++ package.json | 1 + pages/p/[party].tsx | 6 +- types/Character.d.ts | 2 + types/GridCharacter.d.ts | 3 +- types/GridSummon.d.ts | 2 +- 16 files changed, 726 insertions(+), 477 deletions(-) diff --git a/components/CharacterGrid/index.tsx b/components/CharacterGrid/index.tsx index dc7b043f..ba05648b 100644 --- a/components/CharacterGrid/index.tsx +++ b/components/CharacterGrid/index.tsx @@ -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 + partyId?: string + characters: GridArray editable: boolean - exists: boolean - onSelect: (type: GridType, character: Character, position: number) => void + createParty: () => Promise> + 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>({}) + + // 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>({}) + + // 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 (
    - { - Array.from(Array(numCharacters)).map((x, i) => { - return ( -
  • - { openSearchModal(i) }} - editable={props.editable} - position={i} - character={props.grid[i]} - /> -
  • - ) - }) - } + {Array.from(Array(numCharacters)).map((x, i) => { + return ( +
  • + { openSearchModal(i) }} + updateUncap={initiateUncapUpdate} + /> +
  • + ) + })} + {open ? ( - - ) : null} + + ) : null}
) diff --git a/components/CharacterUnit/index.tsx b/components/CharacterUnit/index.tsx index 309b3789..f92bd67a 100644 --- a/components/CharacterUnit/index.tsx +++ b/components/CharacterUnit/index.tsx @@ -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 (
@@ -48,11 +56,15 @@ const CharacterUnit = (props: Props) => { {character?.name.en} { (props.editable) ? : '' }
- + { (gridCharacter && character) ? + : '' }

{character?.name.en}

diff --git a/components/ExtraSummons/index.tsx b/components/ExtraSummons/index.tsx index e32b8e4a..80d21a4b 100644 --- a/components/ExtraSummons/index.tsx +++ b/components/ExtraSummons/index.tsx @@ -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) => { diff --git a/components/ExtraWeapons/index.tsx b/components/ExtraWeapons/index.tsx index 752a94be..efa1dfa5 100644 --- a/components/ExtraWeapons/index.tsx +++ b/components/ExtraWeapons/index.tsx @@ -15,11 +15,10 @@ export enum GridType { interface Props { grid: GridArray 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) => { diff --git a/components/Party/index.tsx b/components/Party/index.tsx index 22677894..51d7eb4e 100644 --- a/components/Party/index.tsx +++ b/components/Party/index.tsx @@ -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 + characters?: GridArray weapons?: GridArray summons?: GridArray 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>({}) - const [weapons, setWeapons] = useState>({}) - const [summons, setSummons] = useState>({}) - - const [mainWeapon, setMainWeapon] = useState() - const [mainSummon, setMainSummon] = useState() - const [friendSummon, setFriendSummon] = useState() - + // Set up states const [extra, setExtra] = useState(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 = ( - - ) - - const summonGrid = ( - - ) - - const characterGrid = ( - - ) - const [currentTab, setCurrentTab] = useState(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) { setExtra(event.target.checked) } + // Methods: Navigating with segmented control function segmentClicked(event: React.ChangeEvent) { 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 = ( + + ) - return party.id - }) - .then(partyId => { - setPartyId(partyId) - saveItem(partyId, type, item, position) - }) - } else { - saveItem(partyId, type, item, position) - } - } + const weaponGrid = ( + + ) - async function createParty() { - const body = (!cookies.user) ? { - party: { - is_extra: extra - } - } : { - party: { - user_id: cookies.user.userId, - is_extra: extra - } - } + const summonGrid = ( + + ) - return await api.endpoints.parties.create(body, headers) - } + const characterGrid = ( + + ) - 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 (
- - - { - (() => { - switch(currentTab) { - case GridType.Character: - return characterGrid - case GridType.Weapon: - return weaponGrid - case GridType.Summon: - return summonGrid - } - })() - } + { navigation } + { currentGrid() }
) } diff --git a/components/SummonGrid/index.tsx b/components/SummonGrid/index.tsx index 64ea5fcc..bce5de33 100644 --- a/components/SummonGrid/index.tsx +++ b/components/SummonGrid/index.tsx @@ -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 + mainSummon: GridSummon | undefined + friendSummon: GridSummon | undefined + summons: GridArray editable: boolean - exists: boolean - found?: boolean - onSelect: (type: GridType, summon: Summon, position: number) => void + createParty: () => Promise> + 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 = 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>({}) + const [mainSummon, setMainSummon] = useState() + const [friendSummon, setFriendSummon] = useState() + // 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>({}) + + // 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 = ( +
+
Main Summon
+ { openSearchModal(-1) }} + updateUncap={initiateUncapUpdate} + /> +
+ ) + + const friendSummonElement = ( +
+
Friend Summon
+ { openSearchModal(6) }} + updateUncap={initiateUncapUpdate} + /> +
+ ) + const summonGridElement = ( +
+
Summons
+
    + {Array.from(Array(numSummons)).map((x, i) => { + return (
  • + { openSearchModal(i) }} + updateUncap={initiateUncapUpdate} + /> +
  • ) + })} +
+
+ ) + const subAuraSummonElement = ( + + ) return (
-
-
Main Summon
- { openSearchModal(-1) }} - updateUncap={initiateUncapUpdate} - /> -
- -
-
Friend Summon
- { openSearchModal(6) }} - updateUncap={initiateUncapUpdate} - /> -
- -
-
Summons
-
    - { - Array.from(Array(numSummons)).map((x, i) => { - return ( -
  • - { openSearchModal(i) }} - updateUncap={initiateUncapUpdate} - /> -
  • - ) - }) - } -
-
+ { mainSummonElement } + { friendSummonElement } + { summonGridElement }
- + { subAuraSummonElement } {open ? ( diff --git a/components/SummonUnit/index.tsx b/components/SummonUnit/index.tsx index 85b19145..c76a9e92 100644 --- a/components/SummonUnit/index.tsx +++ b/components/SummonUnit/index.tsx @@ -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} /> : '' }

{summon?.name.en}

diff --git a/components/UncapIndicator/index.tsx b/components/UncapIndicator/index.tsx index 6073e583..5eb58724 100644 --- a/components/UncapIndicator/index.tsx +++ b/components/UncapIndicator/index.tsx @@ -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 = uncap} key={`star_${i}`} index={i} onClick={toggleStar} /> + return = props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} /> + } + + const ulb = (i: number) => { + return = props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} /> } const flb = (i: number) => { - return = uncap} key={`star_${i}`} index={i} onClick={toggleStar} /> + return = props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} /> } const mlb = (i: number) => { - return = uncap} key={`star_${i}`} index={i} onClick={toggleStar} /> + // console.log("MLB; Number of stars:", props.uncapLevel) + return = props.uncapLevel} key={`star_${i}`} index={i} onClick={toggleStar} /> } - - return (
    { 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) diff --git a/components/WeaponGrid/index.tsx b/components/WeaponGrid/index.tsx index c3e80c6c..a476701f 100644 --- a/components/WeaponGrid/index.tsx +++ b/components/WeaponGrid/index.tsx @@ -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 + mainhand: GridWeapon | undefined + weapons: GridArray extra: boolean editable: boolean - exists: boolean - found?: boolean - onSelect: (type: GridType, weapon: Weapon, position: number) => void + createParty: () => Promise> + 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 = 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>({}) + const [mainWeapon, setMainWeapon] = useState() + // 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>({}) + + // 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 = ( + { openSearchModal(-1) }} + updateUncap={initiateUncapUpdate} + /> + ) + + const weaponGridElement = ( + Array.from(Array(numWeapons)).map((x, i) => { + return ( +
  • + { openSearchModal(i) }} + updateUncap={initiateUncapUpdate} + /> +
  • + ) + }) + ) + + const extraGridElement = ( { return (
    - { openSearchModal(-1) }} - updateUncap={initiateUncapUpdate} - /> - -
      - { - Array.from(Array(numWeapons)).map((x, i) => { - return ( -
    • - { openSearchModal(i) }} - updateUncap={initiateUncapUpdate} - /> -
    • - ) - }) - } -
    + { mainhandElement } +
      { weaponGridElement }
    - { (() => { - if(props.extra) { - return extraGrid - } - })() } + { (() => { return (props.extra) ? extraGridElement : '' })() } {open ? ( diff --git a/components/WeaponUnit/index.tsx b/components/WeaponUnit/index.tsx index fc69e77d..b50e541e 100644 --- a/components/WeaponUnit/index.tsx +++ b/components/WeaponUnit/index.tsx @@ -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} /> : '' }
    diff --git a/package-lock.json b/package-lock.json index efd90a14..5d16aaeb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index f7d84b90..898e39c7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/p/[party].tsx b/pages/p/[party].tsx index 310e66f4..968eed90 100644 --- a/pages/p/[party].tsx +++ b/pages/p/[party].tsx @@ -22,7 +22,7 @@ const PartyRoute: React.FC = () => { const [loading, setLoading] = useState(true) const [editable, setEditable] = useState(false) - const [characters, setCharacters] = useState>({}) + const [characters, setCharacters] = useState>({}) const [weapons, setWeapons] = useState>({}) const [summons, setSummons] = useState>({}) @@ -73,11 +73,11 @@ const PartyRoute: React.FC = () => { } function populateCharacters(list: [GridCharacter]) { - let characters: GridArray = {} + let characters: GridArray = {} list.forEach((object: GridCharacter) => { if (object.position != null) - characters[object.position] = object.character + characters[object.position] = object }) return characters diff --git a/types/Character.d.ts b/types/Character.d.ts index dc7a1ffd..e004581e 100644 --- a/types/Character.d.ts +++ b/types/Character.d.ts @@ -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 } \ No newline at end of file diff --git a/types/GridCharacter.d.ts b/types/GridCharacter.d.ts index 0f3dd5f3..47784b17 100644 --- a/types/GridCharacter.d.ts +++ b/types/GridCharacter.d.ts @@ -1,5 +1,6 @@ interface GridCharacter { id: string - position: number | null + position: number character: Character + uncap_level: number } \ No newline at end of file diff --git a/types/GridSummon.d.ts b/types/GridSummon.d.ts index 6cbb3fe6..0ce8c2fc 100644 --- a/types/GridSummon.d.ts +++ b/types/GridSummon.d.ts @@ -2,7 +2,7 @@ interface GridSummon { id: string main: boolean friend: boolean - position: number | null + position: number summon: Summon uncap_level: number } \ No newline at end of file