diff --git a/components/CharacterGrid/index.tsx b/components/CharacterGrid/index.tsx index d4d31ae0..ebfb9115 100644 --- a/components/CharacterGrid/index.tsx +++ b/components/CharacterGrid/index.tsx @@ -1,254 +1,220 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import React, { useCallback, useEffect, useMemo, useState } from 'react' -import { useCookies } from 'react-cookie' -import { useSnapshot } from 'valtio' +import React, { useCallback, useEffect, useMemo, useState } from "react" +import { getCookie } from "cookies-next" +import { useSnapshot } from "valtio" -import { AxiosResponse } from 'axios' -import debounce from 'lodash.debounce' +import { AxiosResponse } from "axios" +import debounce from "lodash.debounce" -import JobSection from '~components/JobSection' -import CharacterUnit from '~components/CharacterUnit' +import JobSection from "~components/JobSection" +import CharacterUnit from "~components/CharacterUnit" -import api from '~utils/api' -import { appState } from '~utils/appState' +import api from "~utils/api" +import { appState } from "~utils/appState" -import './index.scss' +import "./index.scss" // Props interface Props { - new: boolean - slug?: string - createParty: () => Promise> - pushHistory?: (path: string) => void + new: boolean + characters?: GridCharacter[] + createParty: () => Promise> + pushHistory?: (path: string) => void } const CharacterGrid = (props: Props) => { - // Constants - const numCharacters: number = 5 + // Constants + const numCharacters: number = 5 - // Cookies - const [cookies] = useCookies(['account']) - const headers = (cookies.account != null) ? { - headers: { - 'Authorization': `Bearer ${cookies.account.access_token}` - } - } : {} + // Cookies + const cookie = getCookie("account") + const accountData: AccountCookie = cookie + ? JSON.parse(cookie as string) + : null + const headers = accountData + ? { headers: { Authorization: `Bearer ${accountData.token}` } } + : {} - // Set up state for view management - const { party, grid } = useSnapshot(appState) + // Set up state for view management + const { party, grid } = useSnapshot(appState) + const [slug, setSlug] = useState() - const [slug, setSlug] = useState() - const [found, setFound] = useState(false) - const [loading, setLoading] = useState(true) - const [firstLoadComplete, setFirstLoadComplete] = useState(false) + // Create a temporary state to store previous character uncap values + const [previousUncapValues, setPreviousUncapValues] = useState<{ + [key: number]: number + }>({}) - // Create a temporary state to store previous character uncap values - const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({}) - - // Fetch data from the server - useEffect(() => { - const shortcode = (props.slug) ? props.slug : slug - if (shortcode) fetchGrid(shortcode) - else appState.party.editable = true - }, [slug, props.slug]) + // Set the editable flag only on first load + useEffect(() => { + // If user is logged in and matches + if ( + (accountData && party.user && accountData.userId === party.user.id) || + props.new + ) + appState.party.editable = true + else appState.party.editable = false + }, [props.new, accountData, party]) - // Set the editable flag only on first load - useEffect(() => { - if (!loading && !firstLoadComplete) { - // If user is logged in and matches - if ((cookies.account && party.user && cookies.account.user_id === party.user.id) || props.new) - appState.party.editable = true - else - appState.party.editable = false + // Initialize an array of current uncap values for each characters + useEffect(() => { + let initialPreviousUncapValues: { [key: number]: number } = {} + Object.values(appState.grid.characters).map( + (o) => (initialPreviousUncapValues[o.position] = o.uncap_level) + ) + setPreviousUncapValues(initialPreviousUncapValues) + }, [appState.grid.characters]) - setFirstLoadComplete(true) - } - }, [props.new, cookies, party, loading, firstLoadComplete]) + // Methods: Adding an object from search + function receiveCharacterFromSearch( + object: Character | Weapon | Summon, + position: number + ) { + const character = object as Character - // Initialize an array of current uncap values for each characters - useEffect(() => { - let initialPreviousUncapValues: {[key: number]: number} = {} - Object.values(appState.grid.characters).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) - setPreviousUncapValues(initialPreviousUncapValues) - }, [appState.grid.characters]) - - // Methods: Fetching an object from the server - async function fetchGrid(shortcode: string) { - return api.endpoints.parties.getOneWithObject({ id: shortcode, object: 'characters', params: headers }) - .then(response => processResult(response)) - .catch(error => processError(error)) - } - - function processResult(response: AxiosResponse) { - // Store the response - const party: Party = response.data.party - - // Store the important party and state-keeping values + if (!party.id) { + props.createParty().then((response) => { + const party = response.data.party appState.party.id = party.id - appState.party.user = party.user - appState.party.favorited = party.favorited - appState.party.created_at = party.created_at - appState.party.updated_at = party.updated_at - - setFound(true) - setLoading(false) + setSlug(party.shortcode) - // Populate the weapons in state - populateCharacters(party.characters) + if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) + saveCharacter(party.id, character, position) + .then((response) => storeGridCharacter(response.data.grid_character)) + .catch((error) => console.error(error)) + }) + } else { + if (party.editable) + saveCharacter(party.id, character, position) + .then((response) => storeGridCharacter(response.data.grid_character)) + .catch((error) => console.error(error)) + } + } + + 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, + uncap_level: characterUncapLevel(character), + }, + }, + headers + ) + } + + function storeGridCharacter(gridCharacter: GridCharacter) { + appState.grid.characters[gridCharacter.position] = gridCharacter + } + + // 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 } - function processError(error: any) { - if (error.response != null) { - if (error.response.status == 404) { - setFound(false) - setLoading(false) - } - } else { - console.error(error) - } - } + return uncapLevel + } - function populateCharacters(list: Array) { - list.forEach((object: GridCharacter) => { - if (object.position != null) - appState.grid.characters[object.position] = object + // Methods: Updating uncap level + // Note: Saves, but debouncing is not working properly + async function saveUncap(id: string, position: number, uncapLevel: number) { + storePreviousUncapValue(position) + + try { + if (uncapLevel != previousUncapValues[position]) + await api.updateUncap("character", 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) } + } - // Methods: Adding an object from search - function receiveCharacterFromSearch(object: Character | Weapon | Summon, position: number) { - const character = object as Character + function initiateUncapUpdate( + id: string, + position: number, + uncapLevel: number + ) { + memoizeAction(id, position, uncapLevel) - if (!party.id) { - props.createParty() - .then(response => { - const party = response.data.party - appState.party.id = party.id - setSlug(party.shortcode) + // Optimistically update UI + updateUncapLevel(position, uncapLevel) + } - if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) - saveCharacter(party.id, character, position) - .then(response => storeGridCharacter(response.data.grid_character)) - .catch(error => console.error(error)) - }) - } else { - if (party.editable) - saveCharacter(party.id, character, position) - .then(response => storeGridCharacter(response.data.grid_character)) - .catch(error => console.error(error)) - } + const memoizeAction = useCallback( + (id: string, position: number, uncapLevel: number) => { + debouncedAction(id, position, uncapLevel) + }, + [props, previousUncapValues] + ) + + const debouncedAction = useMemo( + () => + debounce((id, position, number) => { + saveUncap(id, position, number) + }, 500), + [props, saveUncap] + ) + + const updateUncapLevel = (position: number, uncapLevel: number) => { + appState.grid.characters[position].uncap_level = uncapLevel + } + + function storePreviousUncapValue(position: number) { + // Save the current value in case of an unexpected result + let newPreviousValues = { ...previousUncapValues } + + if (grid.characters[position]) { + newPreviousValues[position] = grid.characters[position].uncap_level + setPreviousUncapValues(newPreviousValues) } + } - 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, - 'uncap_level': characterUncapLevel(character) - } - }, headers) - } - - function storeGridCharacter(gridCharacter: GridCharacter) { - appState.grid.characters[gridCharacter.position] = gridCharacter - } - - // 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) { - storePreviousUncapValue(position) - - try { - if (uncapLevel != previousUncapValues[position]) - await api.updateUncap('character', 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) - } - } - - function initiateUncapUpdate(id: string, position: number, uncapLevel: number) { - memoizeAction(id, position, uncapLevel) - - // Optimistically update UI - updateUncapLevel(position, uncapLevel) - } - - const memoizeAction = useCallback( - (id: string, position: number, uncapLevel: number) => { - debouncedAction(id, position, uncapLevel) - }, [props, previousUncapValues] - ) - - const debouncedAction = useMemo(() => - debounce((id, position, number) => { - saveUncap(id, position, number) - }, 500), [props, saveUncap] - ) - - const updateUncapLevel = (position: number, uncapLevel: number) => { - appState.grid.characters[position].uncap_level = uncapLevel - } - - function storePreviousUncapValue(position: number) { - // Save the current value in case of an unexpected result - let newPreviousValues = {...previousUncapValues} - - if (grid.characters[position]) { - newPreviousValues[position] = grid.characters[position].uncap_level - setPreviousUncapValues(newPreviousValues) - } - } - - // Render: JSX components - return ( -
-
- -
    - {Array.from(Array(numCharacters)).map((x, i) => { - return ( -
  • - -
  • - ) - })} -
-
-
- ) + // Render: JSX components + return ( +
+
+ +
    + {Array.from(Array(numCharacters)).map((x, i) => { + return ( +
  • + +
  • + ) + })} +
+
+
+ ) } export default CharacterGrid diff --git a/components/Party/index.tsx b/components/Party/index.tsx index eb5a0695..7f63e547 100644 --- a/components/Party/index.tsx +++ b/components/Party/index.tsx @@ -1,254 +1,286 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react' -import { useRouter } from 'next/router' -import { useSnapshot } from 'valtio' -import { useCookies } from 'react-cookie' -import clonedeep from 'lodash.clonedeep' -import { subscribeKey } from 'valtio/utils' +import React, { useCallback, useEffect, useMemo, useState } from "react" +import { useRouter } from "next/router" +import { useSnapshot } from "valtio" +import { getCookie } from "cookies-next" +import clonedeep from "lodash.clonedeep" -import PartySegmentedControl from '~components/PartySegmentedControl' -import PartyDetails from '~components/PartyDetails' -import WeaponGrid from '~components/WeaponGrid' -import SummonGrid from '~components/SummonGrid' -import CharacterGrid from '~components/CharacterGrid' +import PartySegmentedControl from "~components/PartySegmentedControl" +import PartyDetails from "~components/PartyDetails" +import WeaponGrid from "~components/WeaponGrid" +import SummonGrid from "~components/SummonGrid" +import CharacterGrid from "~components/CharacterGrid" -import api from '~utils/api' -import { appState, initialAppState } from '~utils/appState' -import { GridType, TeamElement } from '~utils/enums' +import api from "~utils/api" +import { appState, initialAppState } from "~utils/appState" +import { GridType, TeamElement } from "~utils/enums" -import './index.scss' -import { AxiosResponse } from 'axios' +import "./index.scss" // Props interface Props { - new?: boolean - slug?: string - pushHistory?: (path: string) => void + new?: boolean + team?: Party + raids: Raid[][] + pushHistory?: (path: string) => void } -const Party = (props: Props) => { - // Cookies - const [cookies] = useCookies(['account']) - const headers = useMemo(() => { - return (cookies.account != null) ? { - headers: { 'Authorization': `Bearer ${cookies.account.access_token}` } - } : {} - }, [cookies.account]) +const Party = (props: Props) => { + // Cookies + const cookie = getCookie("account") + const accountData: AccountCookie = cookie + ? JSON.parse(cookie as string) + : null - // Set up router - const router = useRouter() + const headers = useMemo(() => { + return accountData + ? { headers: { Authorization: `Bearer ${accountData.token}` } } + : {} + }, [accountData]) - // Set up states - const { party } = useSnapshot(appState) - const jobState = party.job + // Set up router + const router = useRouter() - const [job, setJob] = useState() - const [currentTab, setCurrentTab] = useState(GridType.Weapon) + // Set up states + const { party } = useSnapshot(appState) + const jobState = party.job - // Reset state on first load - useEffect(() => { - const resetState = clonedeep(initialAppState) - appState.grid = resetState.grid - }, []) + const [job, setJob] = useState() + const [currentTab, setCurrentTab] = useState(GridType.Weapon) - useEffect(() => { - setJob(jobState) - }, [jobState]) + // Reset state on first load + useEffect(() => { + const resetState = clonedeep(initialAppState) + appState.grid = resetState.grid + if (props.team) storeParty(props.team) + }, []) - useEffect(() => { - jobChanged() - }, [job]) + useEffect(() => { + setJob(jobState) + }, [jobState]) - // Methods: Creating a new party - async function createParty(extra: boolean = false) { - let body = { - party: { - ...(cookies.account) && { user_id: cookies.account.user_id }, - extra: extra - } - } + useEffect(() => { + jobChanged() + }, [job]) - return await api.endpoints.parties.create(body, headers) + // Methods: Creating a new party + async function createParty(extra: boolean = false) { + let body = { + party: { + ...(accountData && { user_id: accountData.userId }), + extra: extra, + }, } - // Methods: Updating the party's details - function checkboxChanged(event: React.ChangeEvent) { - appState.party.extra = event.target.checked + return await api.endpoints.parties.create(body, headers) + } - if (party.id) { - api.endpoints.parties.update(party.id, { - 'party': { 'extra': event.target.checked } - }, headers) - } + // Methods: Updating the party's details + function checkboxChanged(event: React.ChangeEvent) { + appState.party.extra = event.target.checked + + if (party.id) { + api.endpoints.parties.update( + party.id, + { + party: { extra: event.target.checked }, + }, + headers + ) } + } - function jobChanged() { - if (party.id) { - api.endpoints.parties.update(party.id, { - 'party': { 'job_id': (job) ? job.id : '' } - }, headers) - } + function jobChanged() { + if (party.id) { + api.endpoints.parties.update( + party.id, + { + party: { job_id: job ? job.id : "" }, + }, + headers + ) } + } - function updateDetails(name?: string, description?: string, raid?: Raid) { - if (appState.party.name !== name || - appState.party.description !== description || - appState.party.raid?.id !== raid?.id) { - if (appState.party.id) - api.endpoints.parties.update(appState.party.id, { - 'party': { - 'name': name, - 'description': description, - 'raid_id': raid?.id - } - }, headers) - .then(() => { - appState.party.name = name - appState.party.description = description - appState.party.raid = raid - appState.party.updated_at = party.updated_at - }) - } + function updateDetails(name?: string, description?: string, raid?: Raid) { + if ( + appState.party.name !== name || + appState.party.description !== description || + appState.party.raid?.id !== raid?.id + ) { + if (appState.party.id) + api.endpoints.parties + .update( + appState.party.id, + { + party: { + name: name, + description: description, + raid_id: raid?.id, + }, + }, + headers + ) + .then(() => { + appState.party.name = name + appState.party.description = description + appState.party.raid = raid + appState.party.updated_at = party.updated_at + }) } + } - // Deleting the party - function deleteTeam(event: React.MouseEvent) { - if (appState.party.editable && appState.party.id) { - api.endpoints.parties.destroy({ id: appState.party.id, params: headers }) - .then(() => { - // Push to route - router.push('/') + // Deleting the party + function deleteTeam(event: React.MouseEvent) { + if (appState.party.editable && appState.party.id) { + api.endpoints.parties + .destroy({ id: appState.party.id, params: headers }) + .then(() => { + // Push to route + router.push("/") - // Clean state - const resetState = clonedeep(initialAppState) - Object.keys(resetState).forEach((key) => { - appState[key] = resetState[key] - }) + // Clean state + const resetState = clonedeep(initialAppState) + Object.keys(resetState).forEach((key) => { + appState[key] = resetState[key] + }) - // Set party to be editable - appState.party.editable = true - }) - .catch((error) => { - console.error(error) - }) - } + // Set party to be editable + appState.party.editable = true + }) + .catch((error) => { + console.error(error) + }) } + } - // Methods: Navigating with segmented control - function segmentClicked(event: React.ChangeEvent) { - switch(event.target.value) { - case 'class': - setCurrentTab(GridType.Class) - break - case 'characters': - setCurrentTab(GridType.Character) - break - case 'weapons': - setCurrentTab(GridType.Weapon) - break - case 'summons': - setCurrentTab(GridType.Summon) - break - default: - break - } + // Methods: Storing party data + const storeParty = function (party: Party) { + // Store the important party and state-keeping values + appState.party.id = party.id + appState.party.extra = party.extra + appState.party.user = party.user + appState.party.favorited = party.favorited + appState.party.created_at = party.created_at + appState.party.updated_at = party.updated_at + + // Populate state + storeCharacters(party.characters) + storeWeapons(party.weapons) + storeSummons(party.summons) + } + + const storeCharacters = (list: Array) => { + list.forEach((object: GridCharacter) => { + if (object.position != null) + appState.grid.characters[object.position] = object + }) + } + + const storeWeapons = (list: Array) => { + list.forEach((gridObject: GridWeapon) => { + if (gridObject.mainhand) { + appState.grid.weapons.mainWeapon = gridObject + appState.party.element = gridObject.object.element + } else if (!gridObject.mainhand && gridObject.position != null) { + appState.grid.weapons.allWeapons[gridObject.position] = gridObject + } + }) + } + + const storeSummons = (list: Array) => { + list.forEach((gridObject: GridSummon) => { + if (gridObject.main) appState.grid.summons.mainSummon = gridObject + else if (gridObject.friend) + appState.grid.summons.friendSummon = gridObject + else if ( + !gridObject.main && + !gridObject.friend && + gridObject.position != null + ) + appState.grid.summons.allSummons[gridObject.position] = gridObject + }) + } + + // Methods: Navigating with segmented control + function segmentClicked(event: React.ChangeEvent) { + switch (event.target.value) { + case "class": + setCurrentTab(GridType.Class) + break + case "characters": + setCurrentTab(GridType.Character) + break + case "weapons": + setCurrentTab(GridType.Weapon) + break + case "summons": + setCurrentTab(GridType.Summon) + break + default: + break } + } - // Methods: Fetch party details - const processResult = useCallback((response: AxiosResponse) => { - appState.party.id = response.data.party.id - appState.party.user = response.data.party.user - appState.party.favorited = response.data.party.favorited - appState.party.created_at = response.data.party.created_at - appState.party.updated_at = response.data.party.updated_at + // Render: JSX components + const navigation = ( + + ) - // Store the party's user-generated details - appState.party.name = response.data.party.name - appState.party.description = response.data.party.description - appState.party.raid = response.data.party.raid - appState.party.job = response.data.party.job - }, []) + const weaponGrid = ( + + ) - const handleError = useCallback((error: any) => { - if (error.response != null && error.response.status == 404) { - // setFound(false) - } else if (error.response != null) { - console.error(error) - } else { - console.error("There was an error.") - } - }, []) + const summonGrid = ( + + ) - const fetchDetails = useCallback((shortcode: string) => { - return api.endpoints.parties.getOne({ id: shortcode, params: headers }) - .then(response => processResult(response)) - .catch(error => handleError(error)) - }, [headers, processResult, handleError]) + const characterGrid = ( + + ) - useEffect(() => { - const shortcode = (props.slug) ? props.slug : undefined - if (shortcode) fetchDetails(shortcode) - }, [props.slug, fetchDetails]) + const currentGrid = () => { + switch (currentTab) { + case GridType.Character: + return characterGrid + case GridType.Weapon: + return weaponGrid + case GridType.Summon: + return summonGrid + } + } - // Render: JSX components - const navigation = ( - + {navigation} +
{currentGrid()}
+ { + - ) - - const weaponGrid = ( - - ) - - const summonGrid = ( - - ) - - const characterGrid = ( - - ) - - const currentGrid = () => { - switch(currentTab) { - case GridType.Character: - return characterGrid - case GridType.Weapon: - return weaponGrid - case GridType.Summon: - return summonGrid - } - } - - return ( -
- { navigation } -
- { currentGrid() } -
- { } -
- ) + } + + ) } export default Party diff --git a/components/SummonGrid/index.tsx b/components/SummonGrid/index.tsx index f1ee104b..8303801c 100644 --- a/components/SummonGrid/index.tsx +++ b/components/SummonGrid/index.tsx @@ -1,316 +1,286 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import React, { useCallback, useEffect, useMemo, useState } from 'react' -import { useCookies } from 'react-cookie' -import { useSnapshot } from 'valtio' -import { useTranslation } from 'next-i18next' +import React, { useCallback, useEffect, useMemo, useState } from "react" +import { getCookie } from "cookies-next" +import { useSnapshot } from "valtio" +import { useTranslation } from "next-i18next" -import { AxiosResponse } from 'axios' -import debounce from 'lodash.debounce' +import { AxiosResponse } from "axios" +import debounce from "lodash.debounce" -import SummonUnit from '~components/SummonUnit' -import ExtraSummons from '~components/ExtraSummons' +import SummonUnit from "~components/SummonUnit" +import ExtraSummons from "~components/ExtraSummons" -import api from '~utils/api' -import { appState } from '~utils/appState' +import api from "~utils/api" +import { appState } from "~utils/appState" -import './index.scss' +import "./index.scss" // Props interface Props { - new: boolean - slug?: string - createParty: () => Promise> - pushHistory?: (path: string) => void + new: boolean + summons?: GridSummon[] + createParty: () => Promise> + pushHistory?: (path: string) => void } const SummonGrid = (props: Props) => { - // Constants - const numSummons: number = 4 + // Constants + const numSummons: number = 4 - const { t } = useTranslation('common') + // Cookies + const cookie = getCookie("account") + const accountData: AccountCookie = cookie + ? JSON.parse(cookie as string) + : null + const headers = accountData + ? { headers: { Authorization: `Bearer ${accountData.token}` } } + : {} - // Cookies - const [cookies, _] = useCookies(['account']) - const headers = (cookies.account != null) ? { - headers: { - 'Authorization': `Bearer ${cookies.account.access_token}` - } - } : {} + // Localization + const { t } = useTranslation("common") - // Set up state for view management - const { party, grid } = useSnapshot(appState) + // Set up state for view management + const { party, grid } = useSnapshot(appState) + const [slug, setSlug] = useState() - const [slug, setSlug] = useState() - const [found, setFound] = useState(false) - const [loading, setLoading] = useState(true) - const [firstLoadComplete, setFirstLoadComplete] = useState(false) + // Create a temporary state to store previous weapon uncap value + const [previousUncapValues, setPreviousUncapValues] = useState<{ + [key: number]: number + }>({}) - // Create a temporary state to store previous weapon uncap value - const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({}) + // Set the editable flag only on first load + useEffect(() => { + // If user is logged in and matches + if ( + (accountData && party.user && accountData.userId === party.user.id) || + props.new + ) + appState.party.editable = true + else appState.party.editable = false + }, [props.new, accountData, party]) - // Fetch data from the server - useEffect(() => { - const shortcode = (props.slug) ? props.slug : slug - if (shortcode) fetchGrid(shortcode) - else appState.party.editable = true - }, [slug, props.slug]) + // Initialize an array of current uncap values for each summon + useEffect(() => { + let initialPreviousUncapValues: { [key: number]: number } = {} - // Set the editable flag only on first load - useEffect(() => { - if (!loading && !firstLoadComplete) { - // If user is logged in and matches - if ((cookies.account && party.user && cookies.account.user_id === party.user.id) || props.new) - appState.party.editable = true - else - appState.party.editable = false + if (appState.grid.summons.mainSummon) + initialPreviousUncapValues[-1] = + appState.grid.summons.mainSummon.uncap_level - setFirstLoadComplete(true) - } - }, [props.new, cookies, party, loading, firstLoadComplete]) + if (appState.grid.summons.friendSummon) + initialPreviousUncapValues[6] = + appState.grid.summons.friendSummon.uncap_level - // Initialize an array of current uncap values for each summon - useEffect(() => { - let initialPreviousUncapValues: {[key: number]: number} = {} + Object.values(appState.grid.summons.allSummons).map( + (o) => (initialPreviousUncapValues[o.position] = o.uncap_level) + ) - if (appState.grid.summons.mainSummon) - initialPreviousUncapValues[-1] = appState.grid.summons.mainSummon.uncap_level + setPreviousUncapValues(initialPreviousUncapValues) + }, [ + appState.grid.summons.mainSummon, + appState.grid.summons.friendSummon, + appState.grid.summons.allSummons, + ]) - if (appState.grid.summons.friendSummon) - initialPreviousUncapValues[6] = appState.grid.summons.friendSummon.uncap_level + // Methods: Adding an object from search + function receiveSummonFromSearch( + object: Character | Weapon | Summon, + position: number + ) { + const summon = object as Summon - Object.values(appState.grid.summons.allSummons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) - - setPreviousUncapValues(initialPreviousUncapValues) - }, [appState.grid.summons.mainSummon, appState.grid.summons.friendSummon, appState.grid.summons.allSummons]) - - - // Methods: Fetching an object from the server - async function fetchGrid(shortcode: string) { - return api.endpoints.parties.getOneWithObject({ id: shortcode, object: 'summons', params: headers }) - .then(response => processResult(response)) - .catch(error => processError(error)) - } - - function processResult(response: AxiosResponse) { - // Store the response - const party: Party = response.data.party - - // Store the important party and state-keeping values + if (!party.id) { + props.createParty().then((response) => { + const party = response.data.party appState.party.id = party.id - appState.party.user = party.user - appState.party.favorited = party.favorited - appState.party.created_at = party.created_at - appState.party.updated_at = party.updated_at - - setFound(true) - setLoading(false) + setSlug(party.shortcode) - // Populate the weapons in state - populateSummons(party.summons) + if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) + + saveSummon(party.id, summon, position).then((response) => + storeGridSummon(response.data.grid_summon) + ) + }) + } else { + if (party.editable) + saveSummon(party.id, summon, position).then((response) => + storeGridSummon(response.data.grid_summon) + ) } + } - function processError(error: any) { - if (error.response != null) { - if (error.response.status == 404) { - setFound(false) - setLoading(false) - } - } else { - console.error(error) - } - } + 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 - function populateSummons(list: Array) { - list.forEach((gridObject: GridSummon) => { - if (gridObject.main) - appState.grid.summons.mainSummon = gridObject - else if (gridObject.friend) - appState.grid.summons.friendSummon = gridObject - else if (!gridObject.main && !gridObject.friend && gridObject.position != null) - appState.grid.summons.allSummons[gridObject.position] = gridObject + 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 + ) + } + + function storeGridSummon(gridSummon: GridSummon) { + if (gridSummon.position == -1) appState.grid.summons.mainSummon = gridSummon + else if (gridSummon.position == 6) + appState.grid.summons.friendSummon = gridSummon + else appState.grid.summons.allSummons[gridSummon.position] = gridSummon + } + + // Methods: Updating uncap level + // Note: Saves, but debouncing is not working properly + async function saveUncap(id: string, position: number, uncapLevel: number) { + storePreviousUncapValue(position) + + try { + if (uncapLevel != previousUncapValues[position]) + 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) } + } - // Methods: Adding an object from search - function receiveSummonFromSearch(object: Character | Weapon | Summon, position: number) { - const summon = object as Summon + function initiateUncapUpdate( + id: string, + position: number, + uncapLevel: number + ) { + memoizeAction(id, position, uncapLevel) - if (!party.id) { - props.createParty() - .then(response => { - const party = response.data.party - appState.party.id = party.id - setSlug(party.shortcode) + // Optimistically update UI + updateUncapLevel(position, uncapLevel) + } - if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) + const memoizeAction = useCallback( + (id: string, position: number, uncapLevel: number) => { + debouncedAction(id, position, uncapLevel) + }, + [props, previousUncapValues] + ) - saveSummon(party.id, summon, position) - .then(response => storeGridSummon(response.data.grid_summon)) - }) - } else { - if (party.editable) - saveSummon(party.id, summon, position) - .then(response => storeGridSummon(response.data.grid_summon)) - } - } + const debouncedAction = useMemo( + () => + debounce((id, position, number) => { + saveUncap(id, position, number) + }, 500), + [props, saveUncap] + ) - 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 updateUncapLevel = (position: number, uncapLevel: number) => { + if (appState.grid.summons.mainSummon && position == -1) + appState.grid.summons.mainSummon.uncap_level = uncapLevel + else if (appState.grid.summons.friendSummon && position == 6) + appState.grid.summons.friendSummon.uncap_level = uncapLevel + else appState.grid.summons.allSummons[position].uncap_level = uncapLevel + } - function storeGridSummon(gridSummon: GridSummon) { - if (gridSummon.position == -1) - appState.grid.summons.mainSummon = gridSummon - else if (gridSummon.position == 6) - appState.grid.summons.friendSummon = gridSummon - else - appState.grid.summons.allSummons[gridSummon.position] = gridSummon - } + function storePreviousUncapValue(position: number) { + // Save the current value in case of an unexpected result + let newPreviousValues = { ...previousUncapValues } - // Methods: Updating uncap level - // Note: Saves, but debouncing is not working properly - async function saveUncap(id: string, position: number, uncapLevel: number) { - storePreviousUncapValue(position) + if (appState.grid.summons.mainSummon && position == -1) + newPreviousValues[position] = appState.grid.summons.mainSummon.uncap_level + else if (appState.grid.summons.friendSummon && position == 6) + newPreviousValues[position] = + appState.grid.summons.friendSummon.uncap_level + else + newPreviousValues[position] = + appState.grid.summons.allSummons[position].uncap_level - try { - if (uncapLevel != previousUncapValues[position]) - await api.updateUncap('summon', id, uncapLevel) - .then(response => { storeGridSummon(response.data.grid_summon) }) - } catch (error) { - console.error(error) + setPreviousUncapValues(newPreviousValues) + } - // Revert optimistic UI - updateUncapLevel(position, previousUncapValues[position]) + // Render: JSX components + const mainSummonElement = ( +
+
{t("summons.main")}
+ +
+ ) - // Remove optimistic key - let newPreviousValues = {...previousUncapValues} - delete newPreviousValues[position] - setPreviousUncapValues(newPreviousValues) - } - } - - function initiateUncapUpdate(id: string, position: number, uncapLevel: number) { - memoizeAction(id, position, uncapLevel) - - // Optimistically update UI - updateUncapLevel(position, uncapLevel) - } - - const memoizeAction = useCallback( - (id: string, position: number, uncapLevel: number) => { - debouncedAction(id, position, uncapLevel) - }, [props, previousUncapValues] - ) - - const debouncedAction = useMemo(() => - debounce((id, position, number) => { - saveUncap(id, position, number) - }, 500), [props, saveUncap] - ) - - const updateUncapLevel = (position: number, uncapLevel: number) => { - if (appState.grid.summons.mainSummon && position == -1) - appState.grid.summons.mainSummon.uncap_level = uncapLevel - else if (appState.grid.summons.friendSummon && position == 6) - appState.grid.summons.friendSummon.uncap_level = uncapLevel - else - appState.grid.summons.allSummons[position].uncap_level = uncapLevel - } - - function storePreviousUncapValue(position: number) { - // Save the current value in case of an unexpected result - let newPreviousValues = {...previousUncapValues} - - if (appState.grid.summons.mainSummon && position == -1) newPreviousValues[position] = appState.grid.summons.mainSummon.uncap_level - else if (appState.grid.summons.friendSummon && position == 6) newPreviousValues[position] = appState.grid.summons.friendSummon.uncap_level - else newPreviousValues[position] = appState.grid.summons.allSummons[position].uncap_level - - setPreviousUncapValues(newPreviousValues) - } - - // Render: JSX components - const mainSummonElement = ( -
-
{t('summons.main')}
- +
{t("summons.friend")}
+ +
+ ) + const summonGridElement = ( +
+
{t("summons.summons")}
+
    + {Array.from(Array(numSummons)).map((x, i) => { + return ( +
  • + -
- ) + /> + + ) + })} + + + ) + const subAuraSummonElement = ( + + ) + return ( +
+
+ {mainSummonElement} + {friendSummonElement} + {summonGridElement} +
- const friendSummonElement = ( -
-
{t('summons.friend')}
- -
- ) - const summonGridElement = ( -
-
{t('summons.summons')}
-
    - {Array.from(Array(numSummons)).map((x, i) => { - return (
  • - -
  • ) - })} -
-
- ) - const subAuraSummonElement = ( - - ) - return ( -
-
- { mainSummonElement } - { friendSummonElement } - { summonGridElement } -
- - { subAuraSummonElement } -
- ) + {subAuraSummonElement} +
+ ) } export default SummonGrid diff --git a/components/WeaponGrid/index.tsx b/components/WeaponGrid/index.tsx index 3eb6b2a3..e4be7183 100644 --- a/components/WeaponGrid/index.tsx +++ b/components/WeaponGrid/index.tsx @@ -1,286 +1,246 @@ /* eslint-disable react-hooks/exhaustive-deps */ -import React, { useCallback, useEffect, useMemo, useState } from 'react' -import { useCookies } from 'react-cookie' -import { useSnapshot } from 'valtio' +import React, { useCallback, useEffect, useMemo, useState } from "react" +import { getCookie } from "cookies-next" +import { useSnapshot } from "valtio" -import { AxiosResponse } from 'axios' -import debounce from 'lodash.debounce' +import { AxiosResponse } from "axios" +import debounce from "lodash.debounce" -import WeaponUnit from '~components/WeaponUnit' -import ExtraWeapons from '~components/ExtraWeapons' +import WeaponUnit from "~components/WeaponUnit" +import ExtraWeapons from "~components/ExtraWeapons" -import api from '~utils/api' -import { appState } from '~utils/appState' +import api from "~utils/api" +import { appState } from "~utils/appState" -import './index.scss' +import "./index.scss" // Props interface Props { - new: boolean - slug?: string - createParty: (extra: boolean) => Promise> - pushHistory?: (path: string) => void + new: boolean + weapons?: GridWeapon[] + createParty: (extra: boolean) => Promise> + pushHistory?: (path: string) => void } const WeaponGrid = (props: Props) => { - // Constants - const numWeapons: number = 9 + // Constants + const numWeapons: number = 9 - // Cookies - const [cookies] = useCookies(['account']) - const headers = (cookies.account != null) ? { - headers: { - 'Authorization': `Bearer ${cookies.account.access_token}` - } - } : {} + // Cookies + const cookie = getCookie("account") + const accountData: AccountCookie = cookie + ? JSON.parse(cookie as string) + : null + const headers = accountData + ? { headers: { Authorization: `Bearer ${accountData.token}` } } + : {} - // Set up state for view management - const { party, grid } = useSnapshot(appState) + // Set up state for view management + const { party, grid } = useSnapshot(appState) + const [slug, setSlug] = useState() - const [slug, setSlug] = useState() - const [found, setFound] = useState(false) - const [loading, setLoading] = useState(true) - const [firstLoadComplete, setFirstLoadComplete] = useState(false) + // Create a temporary state to store previous weapon uncap values + const [previousUncapValues, setPreviousUncapValues] = useState<{ + [key: number]: number + }>({}) - // Create a temporary state to store previous weapon uncap values - const [previousUncapValues, setPreviousUncapValues] = useState<{[key: number]: number}>({}) + // Set the editable flag only on first load + useEffect(() => { + // If user is logged in and matches + if ( + (accountData && party.user && accountData.userId === party.user.id) || + props.new + ) + appState.party.editable = true + else appState.party.editable = false + }, [props.new, accountData, party]) - // Fetch data from the server - useEffect(() => { - const shortcode = (props.slug) ? props.slug : slug - if (shortcode) fetchGrid(shortcode) - else appState.party.editable = true - }, [slug, props.slug]) + // Initialize an array of current uncap values for each weapon + useEffect(() => { + let initialPreviousUncapValues: { [key: number]: number } = {} - // Set the editable flag only on first load - useEffect(() => { - if (!loading && !firstLoadComplete) { - // If user is logged in and matches - if ((cookies.account && party.user && cookies.account.user_id === party.user.id) || props.new) - appState.party.editable = true - else - appState.party.editable = false + if (appState.grid.weapons.mainWeapon) + initialPreviousUncapValues[-1] = + appState.grid.weapons.mainWeapon.uncap_level - setFirstLoadComplete(true) - } - }, [props.new, cookies, party, loading, firstLoadComplete]) + Object.values(appState.grid.weapons.allWeapons).map( + (o) => (initialPreviousUncapValues[o.position] = o.uncap_level) + ) - // Initialize an array of current uncap values for each weapon - useEffect(() => { - let initialPreviousUncapValues: {[key: number]: number} = {} + setPreviousUncapValues(initialPreviousUncapValues) + }, [appState.grid.weapons.mainWeapon, appState.grid.weapons.allWeapons]) - if (appState.grid.weapons.mainWeapon) - initialPreviousUncapValues[-1] = appState.grid.weapons.mainWeapon.uncap_level + // Methods: Adding an object from search + function receiveWeaponFromSearch( + object: Character | Weapon | Summon, + position: number + ) { + const weapon = object as Weapon + if (position == 1) appState.party.element = weapon.element - Object.values(appState.grid.weapons.allWeapons).map(o => initialPreviousUncapValues[o.position] = o.uncap_level) - - setPreviousUncapValues(initialPreviousUncapValues) - }, [appState.grid.weapons.mainWeapon, appState.grid.weapons.allWeapons]) - - // Methods: Fetching an object from the server - async function fetchGrid(shortcode: string) { - return api.endpoints.parties.getOneWithObject({ id: shortcode, object: 'weapons', params: headers }) - .then(response => processResult(response)) - .catch(error => processError(error)) - } - - function processResult(response: AxiosResponse) { - // Store the response - const party: Party = response.data.party - - // Store the important party and state-keeping values + if (!party.id) { + props.createParty(party.extra).then((response) => { + const party = response.data.party appState.party.id = party.id - appState.party.extra = party.extra - appState.party.user = party.user - appState.party.favorited = party.favorited - appState.party.created_at = party.created_at - appState.party.updated_at = party.updated_at + setSlug(party.shortcode) - setFound(true) - setLoading(false) + if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) - // Populate the weapons in state - populateWeapons(party.weapons) + saveWeapon(party.id, weapon, position).then((response) => + storeGridWeapon(response.data.grid_weapon) + ) + }) + } else { + saveWeapon(party.id, weapon, position).then((response) => + storeGridWeapon(response.data.grid_weapon) + ) } + } - function processError(error: any) { - if (error.response != null) { - if (error.response.status == 404) { - setFound(false) - setLoading(false) - } - } else { - console.error(error) - } + 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 + ) + } + + function storeGridWeapon(gridWeapon: GridWeapon) { + if (gridWeapon.position == -1) { + appState.grid.weapons.mainWeapon = gridWeapon + appState.party.element = gridWeapon.object.element + } else { + // Store the grid unit at the correct position + appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon } + } - function populateWeapons(list: Array) { - list.forEach((gridObject: GridWeapon) => { - if (gridObject.mainhand) { - appState.grid.weapons.mainWeapon = gridObject - appState.party.element = gridObject.object.element - } else if (!gridObject.mainhand && gridObject.position != null) { - appState.grid.weapons.allWeapons[gridObject.position] = gridObject - } + // Methods: Updating uncap level + // Note: Saves, but debouncing is not working properly + async function saveUncap(id: string, position: number, uncapLevel: number) { + storePreviousUncapValue(position) + + try { + if (uncapLevel != previousUncapValues[position]) + 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) } - - // Methods: Adding an object from search - function receiveWeaponFromSearch(object: Character | Weapon | Summon, position: number) { - const weapon = object as Weapon - if (position == 1) - appState.party.element = weapon.element + } - if (!party.id) { - props.createParty(party.extra) - .then(response => { - const party = response.data.party - appState.party.id = party.id - setSlug(party.shortcode) + function initiateUncapUpdate( + id: string, + position: number, + uncapLevel: number + ) { + memoizeAction(id, position, uncapLevel) - if (props.pushHistory) props.pushHistory(`/p/${party.shortcode}`) + // Optimistically update UI + updateUncapLevel(position, uncapLevel) + } - saveWeapon(party.id, weapon, position) - .then(response => storeGridWeapon(response.data.grid_weapon)) - }) - } else { - saveWeapon(party.id, weapon, position) - .then(response => storeGridWeapon(response.data.grid_weapon)) - } - } + const memoizeAction = useCallback( + (id: string, position: number, uncapLevel: number) => { + debouncedAction(id, position, uncapLevel) + }, + [props, previousUncapValues] + ) - 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 = useMemo( + () => + debounce((id, position, number) => { + saveUncap(id, position, number) + }, 500), + [props, saveUncap] + ) - function storeGridWeapon(gridWeapon: GridWeapon) { - if (gridWeapon.position == -1) { - appState.grid.weapons.mainWeapon = gridWeapon - appState.party.element = gridWeapon.object.element - } else { - // Store the grid unit at the correct position - appState.grid.weapons.allWeapons[gridWeapon.position] = gridWeapon - } - } + const updateUncapLevel = (position: number, uncapLevel: number) => { + if (appState.grid.weapons.mainWeapon && position == -1) + appState.grid.weapons.mainWeapon.uncap_level = uncapLevel + else appState.grid.weapons.allWeapons[position].uncap_level = uncapLevel + } - // Methods: Updating uncap level - // Note: Saves, but debouncing is not working properly - async function saveUncap(id: string, position: number, uncapLevel: number) { - storePreviousUncapValue(position) + function storePreviousUncapValue(position: number) { + // Save the current value in case of an unexpected result + let newPreviousValues = { ...previousUncapValues } + newPreviousValues[position] = + appState.grid.weapons.mainWeapon && position == -1 + ? appState.grid.weapons.mainWeapon.uncap_level + : appState.grid.weapons.allWeapons[position].uncap_level + setPreviousUncapValues(newPreviousValues) + } - try { - if (uncapLevel != previousUncapValues[position]) - await api.updateUncap('weapon', id, uncapLevel) - .then(response => { storeGridWeapon(response.data.grid_weapon) }) - } catch (error) { - console.error(error) + // Render: JSX components + const mainhandElement = ( + + ) - // Revert optimistic UI - updateUncapLevel(position, previousUncapValues[position]) - - // Remove optimistic key - let newPreviousValues = {...previousUncapValues} - delete newPreviousValues[position] - setPreviousUncapValues(newPreviousValues) - } - } - - function initiateUncapUpdate(id: string, position: number, uncapLevel: number) { - memoizeAction(id, position, uncapLevel) - - // Optimistically update UI - updateUncapLevel(position, uncapLevel) - } - - const memoizeAction = useCallback( - (id: string, position: number, uncapLevel: number) => { - debouncedAction(id, position, uncapLevel) - }, [props, previousUncapValues] - ) - - const debouncedAction = useMemo(() => - debounce((id, position, number) => { - saveUncap(id, position, number) - }, 500), [props, saveUncap] - ) - - const updateUncapLevel = (position: number, uncapLevel: number) => { - if (appState.grid.weapons.mainWeapon && position == -1) - appState.grid.weapons.mainWeapon.uncap_level = uncapLevel - else - appState.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] = (appState.grid.weapons.mainWeapon && position == -1) ? - appState.grid.weapons.mainWeapon.uncap_level : appState.grid.weapons.allWeapons[position].uncap_level - setPreviousUncapValues(newPreviousValues) - } - - // Render: JSX components - const mainhandElement = ( - - ) - - const weaponGridElement = ( - Array.from(Array(numWeapons)).map((x, i) => { - return ( -
  • - -
  • - ) - }) - ) - - const extraGridElement = ( - - ) - + const weaponGridElement = Array.from(Array(numWeapons)).map((x, i) => { return ( -
    -
    - { mainhandElement } -
      { weaponGridElement }
    -
    - - { (() => { return (party.extra) ? extraGridElement : '' })() } -
    +
  • + +
  • ) + }) + + const extraGridElement = ( + + ) + + return ( +
    +
    + {mainhandElement} +
      {weaponGridElement}
    +
    + + {(() => { + return party.extra ? extraGridElement : "" + })()} +
    + ) } export default WeaponGrid