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.
This commit is contained in:
Justin Edmund 2023-06-18 00:06:25 -07:00
parent d338459cf5
commit 47a867e39a
4 changed files with 594 additions and 0 deletions

View file

@ -0,0 +1,56 @@
.EditTeam.DialogContent {
min-height: 80vh;
.Container.Scrollable {
height: 100%;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.Content {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: $unit-2x;
}
.Fields {
display: flex;
flex-direction: column;
flex-grow: 1;
gap: $unit;
}
.ExtraNotice {
background: var(--extra-purple-bg);
border-radius: $input-corner;
color: var(--extra-purple-text);
font-weight: $medium;
padding: $unit-2x;
}
.DescriptionField {
display: flex;
flex-direction: column;
justify-content: inherit;
gap: $unit;
flex-grow: 1;
.Left {
flex-grow: 0;
}
textarea.Input {
flex-grow: 1;
&::placeholder {
color: var(--text-secondary);
}
}
.Image {
display: none;
}
}
}

View file

@ -0,0 +1,476 @@
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!'
}
value={party?.description}
onChange={handleTextAreaChanged}
ref={descriptionInput}
/>
</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

View file

@ -169,6 +169,39 @@
"confirm": "Confirm"
}
},
"edit_team": {
"title": "Edit team details",
"segments": {
"basic_info": "Basic info",
"properties": "Battle settings"
},
"buttons": {
"add_field": "Add field",
"confirm": "Save details"
},
"labels": {
"name": "Name",
"description": "Description",
"raid": "Battle",
"extra": "Additional weapons",
"charge_attack": "Charge Attack",
"full_auto": "Full Auto",
"auto_guard": "Auto Guard",
"auto_summon": "Auto Summon",
"turn_count": "Number of turns",
"button_count": "Number of buttons pressed",
"chain_count": "Number of chains",
"clear_time": "Clear time"
},
"descriptions": {
"extra": "Additional weapons are controlled by the selected battle"
},
"placeholders": {
"name": "Name your team"
},
"extra_notice": "You can add additional weapons to this team",
"extra_notice_guidebooks": "You can add additional weapons and Sephira Guidebooks to this team"
},
"delete_team": {
"title": "Delete team",
"description": "Are you sure you want to permanently delete this team?",

View file

@ -169,6 +169,35 @@
"confirm": "入れ替える"
}
},
"edit_team": {
"title": "編成の詳細を編集",
"segments": {
"basic_info": "基本情報",
"properties": "バトル設定・結果"
},
"buttons": {
"add_field": "フィールドを追加",
"confirm": "詳細を保存する"
},
"labels": {
"name": "編成名",
"description": "説明",
"raid": "バトル",
"charge_attack": "奥義",
"full_auto": "フルオート",
"auto_guard": "オートガード",
"auto_summon": "オート召喚",
"turn_count": "経過ターン",
"button_count": "ポチ数",
"chain_count": "チェイン数",
"clear_time": "討伐時間"
},
"placeholders": {
"name": "編成に名前を付ける"
},
"extra_notice": "Additional Weapons枠に武器を装備することはできます",
"extra_notice_guidebooks": "Additional Weaponsまたはセフィラ導本を装備することはできます"
},
"delete_team": {
"title": "編成を削除しますか",
"description": "編成を削除する操作は取り消せません。",