* Move min-width to RaidCombobox, not Popover This fixes #318 * Use snapshots to make tokens reactive This fixes #319 * Revert ChatGPT refactor of this method Oops. This code was nice, but it didn't actually assign `false` to keys to be sent to the server. We will revisit this, but it needs to be fixed right now. This fixes #325 * Ignore gacha directory We will probably scrape these images soon. * Add translation for Auto Summon token * Add auto summon token to app state * Set battle settings in state on update Also renames PartyDetails to PartyFooter and makes description reactive * Stop 1password icon from appearing in name field * Use snapshot for reactive Edit party modal * Fix Edit modal placeholder colors * Fix bug with RaidCombobox and Farming Selecting farming then opening the raid combobox *twice* consecutively would put you in no segment, so no raids appeared Fixes #323 * Fix values staying in Edit team even if not saved The values a user entered in the Edit team modal would persist even if the user hit cancel to close the modal. They wouldn't save to the server, but very confusing nonetheless. Now fixed. * Fix unreadable colors in ElementToggle * Fix button alignment in weapon modal * Add text to filters button on small screens The FilterBar showed a left aligned filter icon on mobile for months and it was driving me insane * Remove extraneous code from Header Including the party name, since it's at the top now * Fix Alert at small sizes * Make copy link toast work again * Remove stylesheet links * Fix remix toasts and alerts from both locations The remix toast and alert was barely hooked up and not showing up when invoked from PartyHeader. It now shows up whether you remix your own team (from PartyDropdown) or if you remix another person's team (from PartyHeader).
488 lines
12 KiB
TypeScript
488 lines
12 KiB
TypeScript
import React, { useEffect, useRef, useState } from 'react'
|
||
import { useRouter } from 'next/router'
|
||
import { useSnapshot } from 'valtio'
|
||
import { useTranslation } from 'react-i18next'
|
||
|
||
import {
|
||
Dialog,
|
||
DialogTrigger,
|
||
DialogClose,
|
||
DialogTitle,
|
||
} from '~components/common/Dialog'
|
||
import DialogContent from '~components/common/DialogContent'
|
||
import Button from '~components/common/Button'
|
||
import CharLimitedFieldset from '~components/common/CharLimitedFieldset'
|
||
import DurationInput from '~components/common/DurationInput'
|
||
import InputTableField from '~components/common/InputTableField'
|
||
import RaidCombobox from '~components/raids/RaidCombobox'
|
||
import SegmentedControl from '~components/common/SegmentedControl'
|
||
import Segment from '~components/common/Segment'
|
||
import SwitchTableField from '~components/common/SwitchTableField'
|
||
import TableField from '~components/common/TableField'
|
||
|
||
import type { DetailsObject } from 'types'
|
||
import type { DialogProps } from '@radix-ui/react-dialog'
|
||
|
||
import { appState } from '~utils/appState'
|
||
|
||
import CheckIcon from '~public/icons/Check.svg'
|
||
import CrossIcon from '~public/icons/Cross.svg'
|
||
import './index.scss'
|
||
|
||
interface Props extends DialogProps {
|
||
party?: Party
|
||
updateCallback: (details: DetailsObject) => void
|
||
}
|
||
|
||
const EditPartyModal = ({ updateCallback, ...props }: Props) => {
|
||
// Set up router
|
||
const router = useRouter()
|
||
|
||
// Set up translation
|
||
const { t } = useTranslation('common')
|
||
|
||
// Set up reactive state
|
||
const { party } = useSnapshot(appState)
|
||
|
||
// Refs
|
||
const headerRef = React.createRef<HTMLDivElement>()
|
||
const footerRef = React.createRef<HTMLDivElement>()
|
||
const descriptionInput = useRef<HTMLTextAreaElement>(null)
|
||
|
||
// States: Component
|
||
const [open, setOpen] = useState(false)
|
||
const [errors, setErrors] = useState<{ [key: string]: string }>({
|
||
name: '',
|
||
description: '',
|
||
})
|
||
const [currentSegment, setCurrentSegment] = useState(0)
|
||
|
||
// States: Data
|
||
const [name, setName] = useState('')
|
||
const [description, setDescription] = useState('')
|
||
const [raid, setRaid] = useState<Raid>()
|
||
const [extra, setExtra] = useState(false)
|
||
const [chargeAttack, setChargeAttack] = useState(true)
|
||
const [fullAuto, setFullAuto] = useState(false)
|
||
const [autoGuard, setAutoGuard] = useState(false)
|
||
const [autoSummon, setAutoSummon] = useState(false)
|
||
|
||
const [buttonCount, setButtonCount] = useState<number | undefined>(undefined)
|
||
const [chainCount, setChainCount] = useState<number | undefined>(undefined)
|
||
const [turnCount, setTurnCount] = useState<number | undefined>(undefined)
|
||
const [clearTime, setClearTime] = useState(0)
|
||
|
||
// Hooks
|
||
useEffect(() => {
|
||
persistFromState()
|
||
}, [party])
|
||
|
||
// Methods: Event handlers (Dialog)
|
||
function openChange() {
|
||
if (open) {
|
||
setOpen(false)
|
||
setCurrentSegment(0)
|
||
persistFromState()
|
||
if (props.onOpenChange) props.onOpenChange(false)
|
||
} else {
|
||
setOpen(true)
|
||
if (props.onOpenChange) props.onOpenChange(true)
|
||
}
|
||
}
|
||
|
||
function onEscapeKeyDown(event: KeyboardEvent) {
|
||
event.preventDefault()
|
||
openChange()
|
||
}
|
||
|
||
function onOpenAutoFocus(event: Event) {
|
||
event.preventDefault()
|
||
}
|
||
|
||
// Methods: Event handlers (Fields)
|
||
function handleInputChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||
event.preventDefault()
|
||
|
||
const { name, value } = event.target
|
||
setName(value)
|
||
|
||
let newErrors = errors
|
||
setErrors(newErrors)
|
||
}
|
||
|
||
function handleChargeAttackChanged(checked: boolean) {
|
||
setChargeAttack(checked)
|
||
}
|
||
|
||
function handleFullAutoChanged(checked: boolean) {
|
||
setFullAuto(checked)
|
||
}
|
||
|
||
function handleAutoGuardChanged(checked: boolean) {
|
||
setAutoGuard(checked)
|
||
}
|
||
|
||
function handleAutoSummonChanged(checked: boolean) {
|
||
setAutoSummon(checked)
|
||
}
|
||
|
||
function handleExtraChanged(checked: boolean) {
|
||
setExtra(checked)
|
||
}
|
||
|
||
function handleClearTimeChanged(value: number) {
|
||
if (!isNaN(value)) setClearTime(value)
|
||
}
|
||
|
||
function handleTurnCountChanged(value?: string) {
|
||
if (!value) return
|
||
const numericalValue = parseInt(value)
|
||
if (!isNaN(numericalValue)) setTurnCount(numericalValue)
|
||
}
|
||
|
||
function handleButtonCountChanged(value?: string) {
|
||
if (!value) return
|
||
const numericalValue = parseInt(value)
|
||
if (!isNaN(numericalValue)) setButtonCount(numericalValue)
|
||
}
|
||
|
||
function handleChainCountChanged(value?: string) {
|
||
if (!value) return
|
||
const numericalValue = parseInt(value)
|
||
if (!isNaN(numericalValue)) setChainCount(numericalValue)
|
||
}
|
||
|
||
function handleTextAreaChanged(
|
||
event: React.ChangeEvent<HTMLTextAreaElement>
|
||
) {
|
||
event.preventDefault()
|
||
|
||
const { name, value } = event.target
|
||
let newErrors = errors
|
||
|
||
setErrors(newErrors)
|
||
}
|
||
|
||
function receiveRaid(raid?: Raid) {
|
||
if (raid) {
|
||
setRaid(raid)
|
||
|
||
if (raid.group.extra) setExtra(true)
|
||
else setExtra(false)
|
||
}
|
||
}
|
||
|
||
// Methods: Data methods
|
||
function persistFromState() {
|
||
if (!party) return
|
||
setName(party.name ? party.name : '')
|
||
setDescription(party.description ? party.description : '')
|
||
setRaid(party.raid)
|
||
setAutoGuard(party.autoGuard)
|
||
setAutoSummon(party.autoSummon)
|
||
setFullAuto(party.fullAuto)
|
||
setChargeAttack(party.chargeAttack)
|
||
setClearTime(party.clearTime)
|
||
if (party.turnCount) setTurnCount(party.turnCount)
|
||
if (party.buttonCount) setButtonCount(party.buttonCount)
|
||
if (party.chainCount) setChainCount(party.chainCount)
|
||
}
|
||
|
||
function updateDetails(event: React.MouseEvent) {
|
||
const descriptionValue = descriptionInput.current?.value
|
||
const details: DetailsObject = {
|
||
fullAuto: fullAuto,
|
||
autoGuard: autoGuard,
|
||
autoSummon: autoSummon,
|
||
chargeAttack: chargeAttack,
|
||
clearTime: clearTime,
|
||
buttonCount: buttonCount,
|
||
turnCount: turnCount,
|
||
chainCount: chainCount,
|
||
name: name,
|
||
description: descriptionValue,
|
||
raid: raid,
|
||
extra: extra,
|
||
}
|
||
|
||
updateCallback(details)
|
||
openChange()
|
||
}
|
||
|
||
// Methods: Rendering methods
|
||
const segmentedControl = () => {
|
||
return (
|
||
<SegmentedControl blended={true}>
|
||
<Segment
|
||
groupName="edit_nav"
|
||
name="core"
|
||
selected={currentSegment === 0}
|
||
tabIndex={0}
|
||
onClick={() => setCurrentSegment(0)}
|
||
>
|
||
{t('modals.edit_team.segments.basic_info')}
|
||
</Segment>
|
||
<Segment
|
||
groupName="edit_nav"
|
||
name="properties"
|
||
selected={currentSegment === 1}
|
||
tabIndex={0}
|
||
onClick={() => setCurrentSegment(1)}
|
||
>
|
||
{t('modals.edit_team.segments.properties')}
|
||
</Segment>
|
||
</SegmentedControl>
|
||
)
|
||
}
|
||
|
||
const nameField = () => {
|
||
return (
|
||
<CharLimitedFieldset
|
||
className="Bound"
|
||
fieldName="name"
|
||
placeholder="Name your team"
|
||
value={name}
|
||
limit={50}
|
||
onChange={handleInputChange}
|
||
error={errors.name}
|
||
/>
|
||
)
|
||
}
|
||
|
||
const raidField = () => {
|
||
return (
|
||
<RaidCombobox
|
||
showAllRaidsOption={false}
|
||
currentRaid={raid}
|
||
onChange={receiveRaid}
|
||
/>
|
||
)
|
||
}
|
||
|
||
const extraNotice = () => {
|
||
if (extra) {
|
||
return (
|
||
<div className="ExtraNotice">
|
||
<span className="ExtraNoticeText">
|
||
{raid && raid.group.guidebooks
|
||
? t('modals.edit_team.extra_notice_guidebooks')
|
||
: t('modals.edit_team.extra_notice')}
|
||
</span>
|
||
</div>
|
||
)
|
||
}
|
||
}
|
||
|
||
const descriptionField = () => {
|
||
return (
|
||
<div className="DescriptionField">
|
||
<textarea
|
||
className="Input Bound"
|
||
name="description"
|
||
placeholder={
|
||
'Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediel’s 3 first\nGood luck with RNG!'
|
||
}
|
||
onChange={handleTextAreaChanged}
|
||
ref={descriptionInput}
|
||
defaultValue={description}
|
||
/>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
const chargeAttackField = () => {
|
||
return (
|
||
<SwitchTableField
|
||
name="charge_attack"
|
||
label={t('modals.edit_team.labels.charge_attack')}
|
||
value={chargeAttack}
|
||
onValueChange={handleChargeAttackChanged}
|
||
/>
|
||
)
|
||
}
|
||
|
||
const fullAutoField = () => {
|
||
return (
|
||
<SwitchTableField
|
||
name="full_auto"
|
||
label={t('modals.edit_team.labels.full_auto')}
|
||
value={fullAuto}
|
||
onValueChange={handleFullAutoChanged}
|
||
/>
|
||
)
|
||
}
|
||
|
||
const autoGuardField = () => {
|
||
return (
|
||
<SwitchTableField
|
||
name="auto_guard"
|
||
label={t('modals.edit_team.labels.auto_guard')}
|
||
value={autoGuard}
|
||
onValueChange={handleAutoGuardChanged}
|
||
/>
|
||
)
|
||
}
|
||
|
||
const autoSummonField = () => {
|
||
return (
|
||
<SwitchTableField
|
||
name="auto_summon"
|
||
label={t('modals.edit_team.labels.auto_summon')}
|
||
value={autoSummon}
|
||
onValueChange={handleAutoSummonChanged}
|
||
/>
|
||
)
|
||
}
|
||
|
||
const extraField = () => {
|
||
return (
|
||
<SwitchTableField
|
||
name="extra"
|
||
className="Extra"
|
||
label={t('modals.edit_team.labels.extra')}
|
||
description={t('modals.edit_team.descriptions.extra')}
|
||
value={extra}
|
||
disabled={true}
|
||
onValueChange={handleExtraChanged}
|
||
/>
|
||
)
|
||
}
|
||
|
||
const clearTimeField = () => {
|
||
return (
|
||
<TableField
|
||
className="Numeric"
|
||
name="clear_time"
|
||
label={t('modals.edit_team.labels.clear_time')}
|
||
>
|
||
<DurationInput
|
||
name="clear_time"
|
||
className="Bound"
|
||
value={clearTime}
|
||
onValueChange={(value: number) => handleClearTimeChanged(value)}
|
||
/>
|
||
</TableField>
|
||
)
|
||
}
|
||
|
||
const turnCountField = () => {
|
||
return (
|
||
<InputTableField
|
||
name="turn_count"
|
||
className="Numeric"
|
||
label={t('modals.edit_team.labels.turn_count')}
|
||
placeholder="0"
|
||
type="number"
|
||
value={turnCount}
|
||
onValueChange={handleTurnCountChanged}
|
||
/>
|
||
)
|
||
}
|
||
|
||
const buttonCountField = () => {
|
||
return (
|
||
<InputTableField
|
||
name="button_count"
|
||
className="Numeric"
|
||
label={t('modals.edit_team.labels.button_count')}
|
||
placeholder="0"
|
||
type="number"
|
||
value={buttonCount}
|
||
onValueChange={handleButtonCountChanged}
|
||
/>
|
||
)
|
||
}
|
||
|
||
const chainCountField = () => {
|
||
return (
|
||
<InputTableField
|
||
name="chain_count"
|
||
className="Numeric"
|
||
label={t('modals.edit_team.labels.chain_count')}
|
||
placeholder="0"
|
||
type="number"
|
||
value={chainCount}
|
||
onValueChange={handleChainCountChanged}
|
||
/>
|
||
)
|
||
}
|
||
|
||
const infoPage = () => {
|
||
return (
|
||
<>
|
||
{nameField()}
|
||
{raidField()}
|
||
{extraNotice()}
|
||
{descriptionField()}
|
||
</>
|
||
)
|
||
}
|
||
|
||
const propertiesPage = () => {
|
||
return (
|
||
<>
|
||
{chargeAttackField()}
|
||
{fullAutoField()}
|
||
{autoSummonField()}
|
||
{autoGuardField()}
|
||
{extraField()}
|
||
{clearTimeField()}
|
||
{turnCountField()}
|
||
{buttonCountField()}
|
||
{chainCountField()}
|
||
</>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<Dialog open={open} onOpenChange={openChange}>
|
||
<DialogTrigger asChild>{props.children}</DialogTrigger>
|
||
<DialogContent
|
||
className="EditTeam"
|
||
headerref={headerRef}
|
||
footerref={footerRef}
|
||
onEscapeKeyDown={onEscapeKeyDown}
|
||
onOpenAutoFocus={onOpenAutoFocus}
|
||
>
|
||
<div className="DialogHeader" ref={headerRef}>
|
||
<div className="DialogTop">
|
||
<DialogTitle className="DialogTitle">
|
||
{t('modals.edit_team.title')}
|
||
</DialogTitle>
|
||
</div>
|
||
<DialogClose className="DialogClose" asChild>
|
||
<span>
|
||
<CrossIcon />
|
||
</span>
|
||
</DialogClose>
|
||
</div>
|
||
|
||
<div className="Content">
|
||
{segmentedControl()}
|
||
<div className="Fields">
|
||
{currentSegment === 0 && infoPage()}
|
||
{currentSegment === 1 && propertiesPage()}
|
||
</div>
|
||
</div>
|
||
<div className="DialogFooter" ref={footerRef}>
|
||
<div className="Left"></div>
|
||
<div className="Right Buttons Spaced">
|
||
<Button
|
||
contained={true}
|
||
text={t('buttons.cancel')}
|
||
onClick={openChange}
|
||
/>
|
||
<Button
|
||
contained={true}
|
||
rightAccessoryIcon={<CheckIcon />}
|
||
text={t('modals.edit_team.buttons.confirm')}
|
||
onClick={updateDetails}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)
|
||
}
|
||
|
||
export default EditPartyModal
|