hensei-web/components/party/EditPartyModal/index.tsx
Justin Edmund 835cdfff6f
Implement Edit team modal (#312)
* 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
2023-06-18 01:29:53 -07:00

477 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useRef, useState } from 'react'
import { useRouter } from 'next/router'
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 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 = ({ party, updateCallback, ...props }: Props) => {
// Set up router
const router = useRouter()
const locale = router.locale
// Set up translation
const { t } = useTranslation('common')
// 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 [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(() => {
if (!party) return
setName(party.name)
setRaid(party.raid)
setAutoGuard(party.auto_guard)
setAutoSummon(party.auto_summon)
setFullAuto(party.full_auto)
setChargeAttack(party.charge_attack)
setClearTime(party.clear_time)
if (party.turn_count) setTurnCount(party.turn_count)
if (party.button_count) setButtonCount(party.button_count)
if (party.chain_count) setChainCount(party.chain_count)
}, [party])
// Methods: Event handlers (Dialog)
function openChange() {
if (open) {
setOpen(false)
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 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 Fediels 3 first\nGood luck with RNG!'
}
onChange={handleTextAreaChanged}
ref={descriptionInput}
>
{party ? party.description : ''}
</textarea>
</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