From 47a867e39a410cb2893700fc90b59219aeedcfba Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sun, 18 Jun 2023 00:06:25 -0700 Subject: [PATCH] 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. --- components/party/EditPartyModal/index.scss | 56 +++ components/party/EditPartyModal/index.tsx | 476 +++++++++++++++++++++ public/locales/en/common.json | 33 ++ public/locales/ja/common.json | 29 ++ 4 files changed, 594 insertions(+) create mode 100644 components/party/EditPartyModal/index.scss create mode 100644 components/party/EditPartyModal/index.tsx diff --git a/components/party/EditPartyModal/index.scss b/components/party/EditPartyModal/index.scss new file mode 100644 index 00000000..8c8752e1 --- /dev/null +++ b/components/party/EditPartyModal/index.scss @@ -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; + } + } +} diff --git a/components/party/EditPartyModal/index.tsx b/components/party/EditPartyModal/index.tsx new file mode 100644 index 00000000..e719537a --- /dev/null +++ b/components/party/EditPartyModal/index.tsx @@ -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() + const footerRef = React.createRef() + const descriptionInput = useRef(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() + 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(undefined) + const [chainCount, setChainCount] = useState(undefined) + const [turnCount, setTurnCount] = useState(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) { + 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 + ) { + 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 ( + + setCurrentSegment(0)} + > + {t('modals.edit_team.segments.basic_info')} + + setCurrentSegment(1)} + > + {t('modals.edit_team.segments.properties')} + + + ) + } + + const nameField = () => { + return ( + + ) + } + + const raidField = () => { + return ( + + ) + } + + const extraNotice = () => { + if (extra) { + return ( +
+ + {raid && raid.group.guidebooks + ? t('modals.edit_team.extra_notice_guidebooks') + : t('modals.edit_team.extra_notice')} + +
+ ) + } + } + + const descriptionField = () => { + return ( +
+