From 9b505f5e206a62563a563b33f1b1a213f5030749 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Wed, 23 Feb 2022 01:51:58 -0800 Subject: [PATCH] Implement state management for Weapon grid Summon and Character will be next. I didn't really pay attention to code cleanliness, so I'll try to do a pass before merging the PR --- components/ExtraWeapons/index.tsx | 2 + components/Modal/index.scss | 4 +- components/Party/index.tsx | 25 +- components/PartySegmentedControl/index.tsx | 13 +- components/SearchModal/index.scss | 28 +- components/SearchModal/index.tsx | 296 +++++++++++---------- components/WeaponGrid/index.tsx | 117 ++++---- components/WeaponUnit/index.tsx | 17 +- 8 files changed, 255 insertions(+), 247 deletions(-) diff --git a/components/ExtraWeapons/index.tsx b/components/ExtraWeapons/index.tsx index fbce69c5..d4c25b95 100644 --- a/components/ExtraWeapons/index.tsx +++ b/components/ExtraWeapons/index.tsx @@ -10,6 +10,7 @@ interface Props { found?: boolean offset: number onClick: (position: number) => void + updateObject: (object: Character | Weapon | Summon, position: number) => void updateUncap: (id: string, position: number, uncap: number) => void } @@ -30,6 +31,7 @@ const ExtraWeapons = (props: Props) => { unitType={1} gridWeapon={props.grid[props.offset + i]} onClick={() => { props.onClick(props.offset + i)}} + updateObject={props.updateObject} updateUncap={props.updateUncap} /> diff --git a/components/Modal/index.scss b/components/Modal/index.scss index cb93fc4b..acc22037 100644 --- a/components/Modal/index.scss +++ b/components/Modal/index.scss @@ -22,9 +22,9 @@ overflow-y: auto; padding: $unit * 3; position: relative; - z-index: 10; + z-index: 21; - #ModalTop { + #ModalHeader { display: flex; flex-direction: row; align-items: center; diff --git a/components/Party/index.tsx b/components/Party/index.tsx index 5d8375bc..2e6e2dce 100644 --- a/components/Party/index.tsx +++ b/components/Party/index.tsx @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react' +import { useSnapshot } from 'valtio' import { useCookies } from 'react-cookie' -import PartyContext from '~context/PartyContext' import PartySegmentedControl from '~components/PartySegmentedControl' import WeaponGrid from '~components/WeaponGrid' @@ -9,6 +9,7 @@ import SummonGrid from '~components/SummonGrid' import CharacterGrid from '~components/CharacterGrid' import api from '~utils/api' +import state from '~utils/state' import { GridType, TeamElement } from '~utils/enums' import './index.scss' @@ -29,12 +30,8 @@ const Party = (props: Props) => { } : {} // Set up states + const { party } = useSnapshot(state) const [currentTab, setCurrentTab] = useState(GridType.Weapon) - const [id, setId] = useState('') - const [slug, setSlug] = useState('') - const [element, setElement] = useState(TeamElement.Any) - const [editable, setEditable] = useState(false) - const [hasExtra, setHasExtra] = useState(false) // Methods: Creating a new party async function createParty(extra: boolean = false) { @@ -50,10 +47,12 @@ const Party = (props: Props) => { // Methods: Updating the party's extra flag function checkboxChanged(event: React.ChangeEvent) { - setHasExtra(event.target.checked) - api.endpoints.parties.update(id, { - 'party': { 'is_extra': event.target.checked } - }, headers) + if (party.id) { + state.party.extra = event.target.checked + api.endpoints.parties.update(party.id, { + 'party': { 'is_extra': event.target.checked } + }, headers) + } } // Methods: Navigating with segmented control @@ -122,10 +121,8 @@ const Party = (props: Props) => { return (
- - { navigation } - { currentGrid() } - + { navigation } + { currentGrid() }
) } diff --git a/components/PartySegmentedControl/index.tsx b/components/PartySegmentedControl/index.tsx index 427244d0..ef448d68 100644 --- a/components/PartySegmentedControl/index.tsx +++ b/components/PartySegmentedControl/index.tsx @@ -1,13 +1,14 @@ import React, { useContext } from 'react' import './index.scss' -import PartyContext from '~context/PartyContext' +import state from '~utils/state' import SegmentedControl from '~components/SegmentedControl' import Segment from '~components/Segment' import ToggleSwitch from '~components/ToggleSwitch' import { GridType } from '~utils/enums' +import { useSnapshot } from 'valtio' interface Props { selectedTab: GridType @@ -16,10 +17,10 @@ interface Props { } const PartySegmentedControl = (props: Props) => { - const { editable, element, hasExtra } = useContext(PartyContext) + const { party } = useSnapshot(state) function getElement() { - switch(element) { + switch(party.element) { case 1: return "wind"; break case 2: return "fire"; break case 3: return "water"; break @@ -34,8 +35,8 @@ const PartySegmentedControl = (props: Props) => { Extra @@ -74,7 +75,7 @@ const PartySegmentedControl = (props: Props) => { { (() => { - if (editable && props.selectedTab == GridType.Weapon) { + if (party.editable && props.selectedTab == GridType.Weapon) { return extraToggle } })() diff --git a/components/SearchModal/index.scss b/components/SearchModal/index.scss index 6882a290..ce57bae8 100644 --- a/components/SearchModal/index.scss +++ b/components/SearchModal/index.scss @@ -1,11 +1,11 @@ -.ModalContainer .Modal.SearchModal { +.Modal.Search { display: flex; flex-direction: column; min-height: 420px; min-width: 600px; padding: 0; - #ModalTop { + #ModalHeader { background: $grey-90; gap: $unit; margin: 0; @@ -13,12 +13,28 @@ position: sticky; top: 0; + button { + background: transparent; + border: none; + height: 42px; + padding: 0; + + svg { + height: 24px; + width: 24px; + vertical-align: middle; + } + } + label { width: 100%; .Input { + border: 1px solid $grey-70; + border-radius: $unit / 2; box-sizing: border-box; - padding: 12px 8px; + font-size: $font-regular; + padding: $unit * 1.5; text-align: left; width: 100%; } @@ -26,13 +42,13 @@ } } -.SearchModal #results_container { +.Search.Modal #results_container { margin: 0; max-height: 330px; padding: 0 12px 12px 12px; } -.SearchModal #NoResults { +.Search.Modal #NoResults { display: flex; flex-direction: column; align-items: center; @@ -40,7 +56,7 @@ flex-grow: 1; } -.SearchModal #NoResults h2 { +.Search.Modal #NoResults h2 { color: #ccc; font-size: $font-large; font-weight: 500; diff --git a/components/SearchModal/index.tsx b/components/SearchModal/index.tsx index 29181049..7a2f9945 100644 --- a/components/SearchModal/index.tsx +++ b/components/SearchModal/index.tsx @@ -1,10 +1,11 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' +import { useSnapshot } from 'valtio' -import { createPortal } from 'react-dom' +import state from '~utils/state' import api from '~utils/api' -import Modal from '~components/Modal' -import Overlay from '~components/Overlay' +import * as Dialog from '@radix-ui/react-dialog' + import CharacterResult from '~components/CharacterResult' import WeaponResult from '~components/WeaponResult' import SummonResult from '~components/SummonResult' @@ -13,138 +14,145 @@ import './index.scss' import PlusIcon from '~public/icons/Add.svg' interface Props { - close: () => void send: (object: Character | Weapon | Summon, position: number) => any - grid: GridArray placeholderText: string fromPosition: number - object: 'weapons' | 'characters' | 'summons' + object: 'weapons' | 'characters' | 'summons', + children: React.ReactNode } -interface State { - query: string, - results: { [key: string]: any } - loading: boolean - message: string - totalResults: number -} +const SearchModal = (props: Props) => { + let { grid } = useSnapshot(state) -class SearchModal extends React.Component { - searchInput: React.RefObject + let searchInput = React.createRef() - constructor(props: Props) { - super(props) - this.state = { - query: '', - results: {}, - loading: false, - message: '', - totalResults: 0 + const [pool, setPool] = useState(Array()) + const [open, setOpen] = useState(false) + const [query, setQuery] = useState('') + const [results, setResults] = useState({}) + const [loading, setLoading] = useState(false) + const [message, setMessage] = useState('') + const [totalResults, setTotalResults] = useState(0) + + useEffect(() => { + if (props.object === 'characters') { + setPool(Object.values(grid.characters).map(o => o.character)) + } else if (props.object === 'weapons') { + setPool(Object.values(grid.weapons.allWeapons).map(o => o.weapon)) + } else if (props.object === 'summons') { + setPool(Object.values(grid.summons.allSummons).map(o => o.summon)) } - this.searchInput = React.createRef() + }, [grid, props.object]) + + useEffect(() => { + if (searchInput.current) + searchInput.current.focus() + }, [searchInput]) + + function filterExclusions(object: Character | Weapon | Summon) { + if (pool[props.fromPosition] && + object.granblue_id === pool[props.fromPosition].granblue_id) + return null + else return object } - componentDidMount() { - if (this.searchInput.current) { - this.searchInput.current.focus() - } - } + function inputChanged(event: React.ChangeEvent) { + const text = event.target.value + if (text.length) { + setQuery(text) + setLoading(true) + setMessage('') - filterExclusions = (o: Character | Weapon | Summon) => { - if (this.props.grid[this.props.fromPosition] && - o.granblue_id == this.props.grid[this.props.fromPosition].granblue_id) { - return null - } else return o - } - - fetchResults = (query: string) => { - const excludes = Object.values(this.props.grid).filter(this.filterExclusions).map((o) => { return o.name.en }).join(',') - - api.search(this.props.object, query, excludes) - .then((response) => { - const data = response.data - const totalResults = data.length - this.setState({ - results: data, - totalResults: totalResults, - loading: false - }) - }, (error) => { - this.setState({ - loading: false, - message: error - }) - }) - } - - inputChanged = (event: React.ChangeEvent) => { - const query = event.target.value - if (query.length) { - this.setState({ query, loading: true, message: '' }, () => { - this.fetchResults(query) - }) + if (text.length > 2) + fetchResults() } else { - this.setState({ query, results: {}, message: '' }) + setQuery('') + setResults({}) + setMessage('') } } + + function fetchResults() { + const excludes = Object.values(pool) + .filter(filterExclusions) + .map((o) => { return o.name.en }).join(',') - sendData = (result: Character | Weapon | Summon) => { - this.props.send(result, this.props.fromPosition) - this.props.close() + api.search(props.object, query, excludes) + .then(response => { + setResults(response.data) + setTotalResults(response.data.length) + setLoading(false) + }) + .catch(error => { + setMessage(error) + setLoading(false) + }) } - renderSearchResults = () => { - const { results } = this.state - - switch(this.props.object) { + function sendData(result: Character | Weapon | Summon) { + props.send(result, props.fromPosition) + setOpen(false) + } + + function renderResults() { + switch(props.object) { case 'weapons': - return this.renderWeaponSearchResults(results) - + return renderWeaponSearchResults(results) + break case 'summons': - return this.renderSummonSearchResults(results) - + return renderSummonSearchResults(results) + break case 'characters': - return this.renderCharacterSearchResults(results) + return renderCharacterSearchResults(results) + break } } - renderWeaponSearchResults = (results: { [key: string]: any }) => { - return ( -
    - { results.map( (result: Weapon) => { - return { this.sendData(result) }} /> - })} -
- ) + function renderWeaponSearchResults(results: { [key: string]: any }) { + const elements = results.map((result: Weapon) => { + return { sendData(result) }} + /> + }) + + return (
    {elements}
) } - renderSummonSearchResults = (results: { [key: string]: any }) => { - return ( -
    - { results.map( (result: Summon) => { - return { this.sendData(result) }} /> - })} -
- ) + function renderSummonSearchResults(results: { [key: string]: any }) { + const elements = results.map((result: Summon) => { + return { sendData(result) }} + /> + }) + + return (
    {elements}
) } - renderCharacterSearchResults = (results: { [key: string]: any }) => { - return ( -
    - { results.map( (result: Character) => { - return { this.sendData(result) }} /> - })} -
- ) + function renderCharacterSearchResults(results: { [key: string]: any }) { + const elements = results.map((result: Character) => { + return { sendData(result) }} + /> + }) + + return (
    {elements}
) } - renderEmptyState = () => { + function renderEmptyState() { let string = '' - if (this.state.query === '') { - string = `No ${this.props.object}` + if (query === '') { + string = `No ${props.object}` + } else if (query.length < 3) { + string = `Type at least 3 characters` } else { - string = `No results found for '${this.state.query}'` + string = `No results found for '${query}'` } return ( @@ -153,48 +161,46 @@ class SearchModal extends React.Component { ) } - - render() { - const { query, loading } = this.state - - let content: JSX.Element - if (Object.entries(this.state.results).length == 0) { - content = this.renderEmptyState() - } else { - content = this.renderSearchResults() - } - - return ( - createPortal( -
-
-
-
- - -
- - {content} -
-
- -
, - document.body - ) - ) + + function resetAndClose() { + setQuery('') + setResults({}) + setOpen(true) } + + return ( + + + {props.children} + + +
+ +
+ + + + +
+ { ((Object.entries(results).length == 0) ? renderEmptyState() : renderResults()) } +
+
+ +
+
+ ) } export default SearchModal \ No newline at end of file diff --git a/components/WeaponGrid/index.tsx b/components/WeaponGrid/index.tsx index a15a6863..5ba49383 100644 --- a/components/WeaponGrid/index.tsx +++ b/components/WeaponGrid/index.tsx @@ -1,15 +1,14 @@ /* eslint-disable react-hooks/exhaustive-deps */ import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react' import { useCookies } from 'react-cookie' +import { useSnapshot } from 'valtio' + import { useModal as useModal } from '~utils/useModal' +import state from '~utils/state' import { AxiosResponse } from 'axios' import debounce from 'lodash.debounce' -import AppContext from '~context/AppContext' -import PartyContext from '~context/PartyContext' - -import SearchModal from '~components/SearchModal' import WeaponUnit from '~components/WeaponUnit' import ExtraWeapons from '~components/ExtraWeapons' @@ -36,20 +35,11 @@ const WeaponGrid = (props: Props) => { } : {} // Set up state for view management + const { party, grid } = useSnapshot(state) + + const [slug, setSlug] = useState() const [found, setFound] = useState(false) const [loading, setLoading] = useState(true) - - // Set up the party context - const { setEditable: setAppEditable } = useContext(AppContext) - const { id, setId } = useContext(PartyContext) - const { slug, setSlug } = useContext(PartyContext) - const { editable, setEditable } = useContext(PartyContext) - const { hasExtra, setHasExtra } = useContext(PartyContext) - const { setElement } = useContext(PartyContext) - - // Set up states for Grid data - const [weapons, setWeapons] = useState>({}) - const [mainWeapon, setMainWeapon] = useState() // Set up states for Search const { open, openModal, closeModal } = useModal() @@ -66,28 +56,27 @@ const WeaponGrid = (props: Props) => { const shortcode = (props.slug) ? props.slug : slug if (shortcode) fetchGrid(shortcode) else { - setEditable(true) - setAppEditable(true) + state.party.editable = true } }, [slug, props.slug]) // Initialize an array of current uncap values for each weapon useEffect(() => { let initialPreviousUncapValues: {[key: number]: number} = {} - if (mainWeapon) initialPreviousUncapValues[-1] = mainWeapon.uncap_level - Object.values(weapons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) + if (state.grid.weapons.mainWeapon) initialPreviousUncapValues[-1] = state.grid.weapons.mainWeapon.uncap_level + Object.values(state.grid.weapons.allWeapons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) setPreviousUncapValues(initialPreviousUncapValues) - }, [mainWeapon, weapons]) + }, [state.grid.weapons.mainWeapon, state.grid.weapons.allWeapons]) // Update search grid whenever weapons or the mainhand are updated useEffect(() => { - let newSearchGrid = Object.values(weapons).map((o) => o.weapon) + let newSearchGrid = Object.values(grid.weapons.allWeapons).map((o) => o.weapon) - if (mainWeapon) - newSearchGrid.unshift(mainWeapon.weapon) + if (state.grid.weapons.mainWeapon) + newSearchGrid.unshift(state.grid.weapons.mainWeapon.weapon) setSearchGrid(newSearchGrid) - }, [weapons, mainWeapon]) + }, [state.grid.weapons.mainWeapon, state.grid.weapons.allWeapons]) // Methods: Fetching an object from the server async function fetchGrid(shortcode: string) { @@ -105,13 +94,13 @@ const WeaponGrid = (props: Props) => { const loggedInUser = (cookies.user) ? cookies.user.user_id : '' if (partyUser != undefined && loggedInUser != undefined && partyUser === loggedInUser) { - setEditable(true) - setAppEditable(true) + state.party.editable = true } // Store the important party and state-keeping values - setId(party.id) - setHasExtra(party.is_extra) + state.party.id = party.id + state.party.extra = party.is_extra + setFound(true) setLoading(false) @@ -135,14 +124,12 @@ const WeaponGrid = (props: Props) => { list.forEach((object: GridWeapon) => { if (object.mainhand) { - setMainWeapon(object) - setElement(object.weapon.element) + state.grid.weapons.mainWeapon = object + state.party.element = object.weapon.element } else if (!object.mainhand && object.position != null) { - weapons[object.position] = object + state.grid.weapons.allWeapons[object.position] = object } }) - - setWeapons(weapons) } // Methods: Adding an object from search @@ -153,21 +140,23 @@ const WeaponGrid = (props: Props) => { function receiveWeaponFromSearch(object: Character | Weapon | Summon, position: number) { const weapon = object as Weapon - setElement(weapon.element) + if (position == 1) + state.party.element = weapon.element - if (!id) { - props.createParty(hasExtra) + if (!party.id) { + props.createParty(party.extra) .then(response => { const party = response.data.party - setId(party.id) + state.party.id = party.id setSlug(party.shortcode) if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) + saveWeapon(party.id, weapon, position) .then(response => storeGridWeapon(response.data.grid_weapon)) }) } else { - saveWeapon(id, weapon, position) + saveWeapon(party.id, weapon, position) .then(response => storeGridWeapon(response.data.grid_weapon)) } } @@ -190,12 +179,11 @@ const WeaponGrid = (props: Props) => { function storeGridWeapon(gridWeapon: GridWeapon) { if (gridWeapon.position == -1) { - setMainWeapon(gridWeapon) + state.grid.weapons.mainWeapon = gridWeapon + state.party.element = gridWeapon.weapon.element } else { // Store the grid unit at the correct position - let newWeapons = Object.assign({}, weapons) - newWeapons[gridWeapon.position] = gridWeapon - setWeapons(newWeapons) + state.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon } } @@ -241,32 +229,30 @@ const WeaponGrid = (props: Props) => { ) 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) - } + if (state.grid.weapons.mainWeapon && position == -1) + state.grid.weapons.mainWeapon.uncap_level = uncapLevel + else + state.grid.weapons.allWeapons[position].uncap_level = uncapLevel } function storePreviousUncapValue(position: number) { // 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 + newPreviousValues[position] = (state.grid.weapons.mainWeapon && position == -1) ? + state.grid.weapons.mainWeapon.uncap_level : state.grid.weapons.allWeapons[position].uncap_level setPreviousUncapValues(newPreviousValues) } // Render: JSX components const mainhandElement = ( { openSearchModal(-1) }} + updateObject={receiveWeaponFromSearch} updateUncap={initiateUncapUpdate} /> ) @@ -276,11 +262,12 @@ const WeaponGrid = (props: Props) => { return (
  • { openSearchModal(i) }} + updateObject={receiveWeaponFromSearch} updateUncap={initiateUncapUpdate} />
  • @@ -290,10 +277,11 @@ const WeaponGrid = (props: Props) => { const extraGridElement = ( ) @@ -305,18 +293,7 @@ const WeaponGrid = (props: Props) => {
      { weaponGridElement }
    - { (() => { return (hasExtra) ? extraGridElement : '' })() } - - {open ? ( - - ) : null} + { (() => { return (party.extra) ? extraGridElement : '' })() } ) } diff --git a/components/WeaponUnit/index.tsx b/components/WeaponUnit/index.tsx index 396ceeaa..00008196 100644 --- a/components/WeaponUnit/index.tsx +++ b/components/WeaponUnit/index.tsx @@ -5,6 +5,7 @@ import UncapIndicator from '~components/UncapIndicator' import PlusIcon from '~public/icons/Add.svg' import './index.scss' +import SearchModal from '~components/SearchModal' interface Props { gridWeapon: GridWeapon | undefined @@ -12,6 +13,7 @@ interface Props { position: number editable: boolean onClick: () => void + updateObject: (object: Character | Weapon | Summon, position: number) => void updateUncap: (id: string, position: number, uncap: number) => void } @@ -55,10 +57,17 @@ const WeaponUnit = (props: Props) => { return (
    -
    {} }> - {weapon?.name.en} - { (props.editable) ? : '' } -
    + +
    {} }> + {weapon?.name.en} + { (props.editable) ? : '' } +
    +
    + { (gridWeapon) ?