* Small refactor to CharLimitedFieldset Some methods were renamed for clarity. <input> props are actually put on the input properly. * Add tabindex to Popover trigger * Add tabindex to Switch and SwitchTableField * Add tabindex to DurationInput * Add new properties * Added guidebooks to RaidGroup * Added auto_summon to Party * Conditionally render description in TableField * Improve SwitchTableField * Add support for passing in classes * Add support for passing a disabled prop * Pass description to TableField * Right-align switch * Add support for Extra color switch * Align SliderTableField input to right * Align SelectTableField input to right * Update placeholder styles * Fix empty state on DurationInput * Remove tabindex from DurationInput * Update InputTableField Allow for passing down input properties and remove fixed width * Fix dialog footer styles * Update dialog and overlay z-index * Add styles to TableField Added styles for numeric inputs, disabled inputs, and generally cleaning things up * Add guidebooks to RaidCombobox + styles * Added guidebooks to the dummy raid group * Fix background color * Make less tall * Implement EditPartyModal EditPartyModal takes functionality that was in PartyHeader and puts it in a modal dialog. This lets us add fields and reduces the complexity of other components. Translations were also added. * Remove edit functionality * Add darker shadow to Select * Properly send raid ID to server * Show Extra grids based on selected raid * Fix EX badge colors * Use child as value in normal textarea * Remove toggle ability from Extra grids * Remove edit functionality from PartyDetails
464 lines
12 KiB
TypeScript
464 lines
12 KiB
TypeScript
import React, { useEffect, useState } from 'react'
|
|
import { getCookie } from 'cookies-next'
|
|
import { useRouter } from 'next/router'
|
|
import { subscribe, useSnapshot } from 'valtio'
|
|
import { useTranslation } from 'next-i18next'
|
|
import clonedeep from 'lodash.clonedeep'
|
|
|
|
import Alert from '~components/common/Alert'
|
|
import PartySegmentedControl from '~components/party/PartySegmentedControl'
|
|
import PartyDetails from '~components/party/PartyDetails'
|
|
import PartyHeader from '~components/party/PartyHeader'
|
|
import WeaponGrid from '~components/weapon/WeaponGrid'
|
|
import SummonGrid from '~components/summon/SummonGrid'
|
|
import CharacterGrid from '~components/character/CharacterGrid'
|
|
|
|
import api from '~utils/api'
|
|
import { accountState } from '~utils/accountState'
|
|
import { appState, initialAppState } from '~utils/appState'
|
|
import { getLocalId } from '~utils/localId'
|
|
import { GridType } from '~utils/enums'
|
|
import { retrieveCookies } from '~utils/retrieveCookies'
|
|
import { setEditKey, storeEditKey, unsetEditKey } from '~utils/userToken'
|
|
|
|
import type { DetailsObject } from '~types'
|
|
|
|
import './index.scss'
|
|
|
|
// Props
|
|
interface Props {
|
|
new?: boolean
|
|
team?: Party
|
|
selectedTab: GridType
|
|
pushHistory?: (path: string) => void
|
|
}
|
|
|
|
const defaultProps = {
|
|
selectedTab: GridType.Weapon,
|
|
}
|
|
|
|
const Party = (props: Props) => {
|
|
// Set up router
|
|
const router = useRouter()
|
|
|
|
// Localization
|
|
const { t } = useTranslation('common')
|
|
|
|
// Set up states
|
|
const { party } = useSnapshot(appState)
|
|
const [editable, setEditable] = useState(false)
|
|
const [currentTab, setCurrentTab] = useState<GridType>(GridType.Weapon)
|
|
const [refresh, setRefresh] = useState(false)
|
|
const [errorMessage, setErrorMessage] = useState('')
|
|
|
|
// Retrieve cookies
|
|
const cookies = retrieveCookies()
|
|
|
|
// Reset state on first load
|
|
useEffect(() => {
|
|
const resetState = clonedeep(initialAppState)
|
|
appState.grid = resetState.grid
|
|
if (props.team) storeParty(props.team)
|
|
}, [])
|
|
|
|
// Subscribe to app state to listen for account changes and
|
|
// unsubscribe when component is unmounted
|
|
const unsubscribe = subscribe(accountState, () => {
|
|
setRefresh(true)
|
|
})
|
|
|
|
useEffect(() => () => unsubscribe(), [])
|
|
|
|
// Set editable on first load
|
|
useEffect(() => {
|
|
// Get cookie
|
|
const cookie = getCookie('account')
|
|
const accountData: AccountCookie = cookie
|
|
? JSON.parse(cookie as string)
|
|
: null
|
|
|
|
let editable = false
|
|
unsetEditKey()
|
|
|
|
if (props.new) editable = true
|
|
|
|
if (accountData && props.team && !props.new) {
|
|
if (accountData.token) {
|
|
// Authenticated
|
|
if (props.team.user && accountData.userId === props.team.user.id) {
|
|
editable = true
|
|
}
|
|
} else {
|
|
// Not authenticated
|
|
if (!props.team.user && accountData.userId === props.team.local_id) {
|
|
// Set editable
|
|
editable = true
|
|
|
|
// Also set edit key header
|
|
setEditKey(props.team.id, props.team.user)
|
|
}
|
|
}
|
|
}
|
|
|
|
appState.party.editable = editable
|
|
setEditable(editable)
|
|
}, [refresh])
|
|
|
|
// Set selected tab from props
|
|
useEffect(() => {
|
|
setCurrentTab(props.selectedTab)
|
|
}, [props.selectedTab])
|
|
|
|
// Methods: Creating a new party
|
|
async function createParty(details?: DetailsObject) {
|
|
let payload = {}
|
|
if (details) payload = formatDetailsObject(details)
|
|
|
|
return await api.endpoints.parties
|
|
.create({ ...payload, ...getLocalId() })
|
|
.then((response) => storeParty(response.data.party))
|
|
}
|
|
|
|
async function updateParty(details: DetailsObject) {
|
|
const payload = formatDetailsObject(details)
|
|
|
|
if (props.team && props.team.id) {
|
|
return await api.endpoints.parties
|
|
.update(props.team.id, payload)
|
|
.then((response) => storeParty(response.data.party))
|
|
.catch((error) => {
|
|
const data = error.response.data
|
|
if (data.errors && Object.keys(data.errors).includes('guidebooks')) {
|
|
const message = t('errors.validation.guidebooks')
|
|
setErrorMessage(message)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Methods: Updating the party's details
|
|
async function updateDetails(details: DetailsObject) {
|
|
if (!props.team) return await createParty(details)
|
|
else updateParty(details)
|
|
}
|
|
|
|
function formatDetailsObject(details: DetailsObject) {
|
|
const payload: { [key: string]: any } = {}
|
|
|
|
const mappings: { [key: string]: string } = {
|
|
name: 'name',
|
|
description: 'description',
|
|
chargeAttack: 'charge_attack',
|
|
fullAuto: 'full_auto',
|
|
autoGuard: 'auto_guard',
|
|
autoSummon: 'auto_summon',
|
|
clearTime: 'clear_time',
|
|
buttonCount: 'button_count',
|
|
chainCount: 'chain_count',
|
|
turnCount: 'turn_count',
|
|
extra: 'extra',
|
|
job: 'job_id',
|
|
guidebook1_id: 'guidebook1_id',
|
|
guidebook2_id: 'guidebook2_id',
|
|
guidebook3_id: 'guidebook3_id',
|
|
}
|
|
|
|
Object.entries(mappings).forEach(([key, value]) => {
|
|
if (details[key]) {
|
|
payload[value] = details[key]
|
|
}
|
|
})
|
|
|
|
if (details.raid) payload.raid_id = details.raid.id
|
|
|
|
if (Object.keys(payload).length >= 1) {
|
|
return { party: payload }
|
|
} else {
|
|
return {}
|
|
}
|
|
}
|
|
|
|
function cancelAlert() {
|
|
setErrorMessage('')
|
|
}
|
|
|
|
function checkboxChanged(enabled: boolean) {
|
|
appState.party.extra = enabled
|
|
|
|
// Only save if this is a saved party
|
|
if (props.team && props.team.id) {
|
|
api.endpoints.parties.update(props.team.id, {
|
|
party: { extra: enabled },
|
|
})
|
|
}
|
|
}
|
|
|
|
function updateGuidebook(book: Guidebook | undefined, position: number) {
|
|
let id: string | undefined = ''
|
|
|
|
if (book) id = book.id
|
|
else if (!book) id = 'undefined'
|
|
else id = undefined
|
|
|
|
const details: DetailsObject = {
|
|
guidebook1_id: position === 1 ? id : undefined,
|
|
guidebook2_id: position === 2 ? id : undefined,
|
|
guidebook3_id: position === 3 ? id : undefined,
|
|
}
|
|
|
|
if (props.team && props.team.id) {
|
|
updateParty(details)
|
|
} else {
|
|
createParty(details)
|
|
}
|
|
}
|
|
|
|
// Remixing the party
|
|
function remixTeam() {
|
|
// setOriginalName(partySnapshot.name ? partySnapshot.name : t('no_title'))
|
|
|
|
if (props.team && props.team.shortcode) {
|
|
const body = getLocalId()
|
|
api
|
|
.remix({ shortcode: props.team.shortcode, body: body })
|
|
.then((response) => {
|
|
const remix = response.data.party
|
|
|
|
// Store the edit key in local storage
|
|
if (remix.edit_key) {
|
|
storeEditKey(remix.id, remix.edit_key)
|
|
setEditKey(remix.id, remix.user)
|
|
}
|
|
|
|
router.push(`/p/${remix.shortcode}`)
|
|
// setRemixToastOpen(true)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Deleting the party
|
|
function deleteTeam() {
|
|
if (props.team && editable) {
|
|
api.endpoints.parties
|
|
.destroy({ id: props.team.id })
|
|
.then(() => {
|
|
// Push to route
|
|
if (cookies && cookies.account.username) {
|
|
router.push(`/${cookies.account.username}`)
|
|
} else {
|
|
router.push('/')
|
|
}
|
|
|
|
// 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)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Methods: Storing party data
|
|
const storeParty = function (team: any) {
|
|
// Store the important party and state-keeping values in global state
|
|
appState.party.name = team.name
|
|
appState.party.description = team.description
|
|
appState.party.raid = team.raid
|
|
appState.party.updated_at = team.updated_at
|
|
appState.party.job = team.job
|
|
appState.party.jobSkills = team.job_skills
|
|
appState.party.accessory = team.accessory
|
|
|
|
appState.party.id = team.id
|
|
appState.party.shortcode = team.shortcode
|
|
appState.party.extra = team.extra
|
|
appState.party.guidebooks = team.guidebooks
|
|
appState.party.user = team.user
|
|
appState.party.favorited = team.favorited
|
|
appState.party.remix = team.remix
|
|
appState.party.remixes = team.remixes
|
|
appState.party.sourceParty = team.source_party
|
|
appState.party.created_at = team.created_at
|
|
appState.party.updated_at = team.updated_at
|
|
|
|
appState.party.detailsVisible = false
|
|
|
|
// Store the edit key in local storage
|
|
if (team.edit_key) {
|
|
storeEditKey(team.id, team.edit_key)
|
|
setEditKey(team.id, team.user)
|
|
}
|
|
|
|
// Populate state
|
|
storeCharacters(team.characters)
|
|
storeWeapons(team.weapons)
|
|
storeSummons(team.summons)
|
|
|
|
// Create a string to send the user back to the tab they're currently on
|
|
let tab = ''
|
|
if (currentTab === GridType.Character) {
|
|
tab = 'characters'
|
|
} else if (currentTab === GridType.Summon) {
|
|
tab = 'summons'
|
|
}
|
|
|
|
// Then, push the browser history to the new party's URL
|
|
if (props.pushHistory) {
|
|
props.pushHistory(`/p/${team.shortcode}/${tab}`)
|
|
}
|
|
|
|
return team
|
|
}
|
|
|
|
const storeCharacters = (list: Array<GridCharacter>) => {
|
|
list.forEach((object: GridCharacter) => {
|
|
if (object.position != null)
|
|
appState.grid.characters[object.position] = object
|
|
})
|
|
}
|
|
|
|
const storeWeapons = (list: Array<GridWeapon>) => {
|
|
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<GridSummon>) => {
|
|
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<HTMLInputElement>) {
|
|
const path = [
|
|
router.asPath.split('/').filter((el) => el != '')[1],
|
|
event.target.value,
|
|
].join('/')
|
|
|
|
switch (event.target.value) {
|
|
case 'characters':
|
|
setCurrentTab(GridType.Character)
|
|
break
|
|
case 'weapons':
|
|
setCurrentTab(GridType.Weapon)
|
|
break
|
|
case 'summons':
|
|
setCurrentTab(GridType.Summon)
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
|
|
router.replace(path, undefined, { shallow: true })
|
|
}
|
|
|
|
// Render: JSX components
|
|
const errorAlert = () => {
|
|
return (
|
|
<Alert
|
|
open={errorMessage.length > 0}
|
|
message={errorMessage}
|
|
cancelAction={cancelAlert}
|
|
cancelActionText={t('buttons.confirm')}
|
|
/>
|
|
)
|
|
}
|
|
|
|
const navigation = (
|
|
<PartySegmentedControl selectedTab={currentTab} onClick={segmentClicked} />
|
|
)
|
|
|
|
const weaponGrid = (
|
|
<WeaponGrid
|
|
new={props.new || false}
|
|
editable={editable}
|
|
weapons={props.team?.weapons}
|
|
guidebooks={props.team?.guidebooks}
|
|
createParty={createParty}
|
|
pushHistory={props.pushHistory}
|
|
updateExtra={checkboxChanged}
|
|
updateGuidebook={updateGuidebook}
|
|
/>
|
|
)
|
|
|
|
const summonGrid = (
|
|
<SummonGrid
|
|
new={props.new || false}
|
|
editable={editable}
|
|
summons={props.team?.summons}
|
|
createParty={createParty}
|
|
pushHistory={props.pushHistory}
|
|
/>
|
|
)
|
|
|
|
const characterGrid = (
|
|
<CharacterGrid
|
|
new={props.new || false}
|
|
editable={editable}
|
|
characters={props.team?.characters}
|
|
createParty={createParty}
|
|
pushHistory={props.pushHistory}
|
|
/>
|
|
)
|
|
|
|
const currentGrid = () => {
|
|
switch (currentTab) {
|
|
case GridType.Character:
|
|
return characterGrid
|
|
case GridType.Weapon:
|
|
return weaponGrid
|
|
case GridType.Summon:
|
|
return summonGrid
|
|
}
|
|
}
|
|
|
|
return (
|
|
<React.Fragment>
|
|
{errorAlert()}
|
|
|
|
<PartyHeader
|
|
party={props.team}
|
|
new={props.new || false}
|
|
editable={party.editable}
|
|
deleteCallback={deleteTeam}
|
|
remixCallback={remixTeam}
|
|
updateCallback={updateDetails}
|
|
/>
|
|
|
|
{navigation}
|
|
|
|
<section id="Party">{currentGrid()}</section>
|
|
|
|
<PartyDetails
|
|
party={props.team}
|
|
new={props.new || false}
|
|
editable={party.editable}
|
|
updateCallback={updateDetails}
|
|
/>
|
|
</React.Fragment>
|
|
)
|
|
}
|
|
|
|
Party.defaultProps = defaultProps
|
|
|
|
export default Party
|