Update EditPartyModal

* Directly adds shadow code from DialogHeader since this dialog behaves slightly differently. In the future, we'd like to reconcile this so that the code only appears once
* Changes rendering functions to be properties
* Add DialogHeader and DialogFooter
* Implement Textarea component instead of raw textarea
* Removed unused code
This commit is contained in:
Justin Edmund 2023-07-02 02:30:59 -07:00
parent d194b54836
commit 7db02886fa
2 changed files with 306 additions and 267 deletions

View file

@ -1,56 +1,39 @@
.EditTeam.DialogContent { .segmentedControlWrapper {
min-height: 80vh;
.Container.Scrollable {
height: 100%;
display: flex; display: flex;
flex-direction: column; align-items: center;
flex-grow: 1; justify-content: center;
padding: $unit 0;
} }
.Content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-grow: 1; flex-grow: 1;
gap: $unit-2x; gap: $unit-2x;
} height: 100%;
overflow: hidden;
.Fields { .fields {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-grow: 1; flex-grow: 1;
gap: $unit; gap: $unit;
padding: 0 $unit-4x;
overflow: hidden;
&.scrollable {
overflow-y: auto;
}
}
} }
.ExtraNotice { .extraNotice {
background: var(--extra-purple-bg); background: var(--extra-purple-bg);
border-radius: $input-corner; border-radius: $input-corner;
color: var(--extra-purple-text);
font-weight: $medium; font-weight: $medium;
padding: $unit-2x; padding: $unit-2x;
}
.DescriptionField { p {
display: flex; color: var(--extra-purple-dark-text);
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

@ -2,16 +2,14 @@ import React, { useEffect, useRef, useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import classNames from 'classnames'
import debounce from 'lodash.debounce'
import { import { Dialog, DialogTrigger } from '~components/common/Dialog'
Dialog, import DialogHeader from '~components/common/DialogHeader'
DialogTrigger, import DialogFooter from '~components/common/DialogFooter'
DialogClose,
DialogTitle,
} from '~components/common/Dialog'
import DialogContent from '~components/common/DialogContent' import DialogContent from '~components/common/DialogContent'
import Button from '~components/common/Button' import Button from '~components/common/Button'
import CharLimitedFieldset from '~components/common/CharLimitedFieldset'
import DurationInput from '~components/common/DurationInput' import DurationInput from '~components/common/DurationInput'
import InputTableField from '~components/common/InputTableField' import InputTableField from '~components/common/InputTableField'
import RaidCombobox from '~components/raids/RaidCombobox' import RaidCombobox from '~components/raids/RaidCombobox'
@ -19,6 +17,7 @@ import SegmentedControl from '~components/common/SegmentedControl'
import Segment from '~components/common/Segment' import Segment from '~components/common/Segment'
import SwitchTableField from '~components/common/SwitchTableField' import SwitchTableField from '~components/common/SwitchTableField'
import TableField from '~components/common/TableField' import TableField from '~components/common/TableField'
import Textarea from '~components/common/Textarea'
import type { DetailsObject } from 'types' import type { DetailsObject } from 'types'
import type { DialogProps } from '@radix-ui/react-dialog' import type { DialogProps } from '@radix-ui/react-dialog'
@ -26,8 +25,8 @@ import type { DialogProps } from '@radix-ui/react-dialog'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import CheckIcon from '~public/icons/Check.svg' import CheckIcon from '~public/icons/Check.svg'
import CrossIcon from '~public/icons/Cross.svg'
import styles from './index.module.scss' import styles from './index.module.scss'
import Input from '~components/common/Input'
interface Props extends DialogProps { interface Props extends DialogProps {
party?: Party party?: Party
@ -46,8 +45,9 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
// Refs // Refs
const headerRef = React.createRef<HTMLDivElement>() const headerRef = React.createRef<HTMLDivElement>()
const topContainerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>() const footerRef = React.createRef<HTMLDivElement>()
const descriptionInput = useRef<HTMLTextAreaElement>(null) const descriptionInput = useRef<HTMLDivElement>(null)
// States: Component // States: Component
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@ -72,6 +72,12 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
const [turnCount, setTurnCount] = useState<number | undefined>(undefined) const [turnCount, setTurnCount] = useState<number | undefined>(undefined)
const [clearTime, setClearTime] = useState(0) const [clearTime, setClearTime] = useState(0)
// Classes
const fieldsClasses = classNames({
[styles.fields]: true,
[styles.scrollable]: currentSegment === 1,
})
// Hooks // Hooks
useEffect(() => { useEffect(() => {
persistFromState() persistFromState()
@ -152,14 +158,9 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
if (!isNaN(numericalValue)) setChainCount(numericalValue) if (!isNaN(numericalValue)) setChainCount(numericalValue)
} }
function handleTextAreaChanged( function handleTextAreaChanged(event: React.ChangeEvent<HTMLDivElement>) {
event: React.ChangeEvent<HTMLTextAreaElement>
) {
event.preventDefault() event.preventDefault()
const { name, value } = event.target
let newErrors = errors let newErrors = errors
setErrors(newErrors) setErrors(newErrors)
} }
@ -172,6 +173,101 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
} }
} }
// Handlers
function handleScroll(event: React.UIEvent<HTMLDivElement, UIEvent>) {
const scrollTop = event.currentTarget.scrollTop
const scrollHeight = event.currentTarget.scrollHeight
const clientHeight = event.currentTarget.clientHeight
if (topContainerRef && topContainerRef.current)
manipulateHeaderShadow(topContainerRef.current, scrollTop)
if (footerRef && footerRef.current)
manipulateFooterShadow(
footerRef.current,
scrollTop,
scrollHeight,
clientHeight
)
}
function manipulateHeaderShadow(header: HTMLDivElement, scrollTop: number) {
const boxShadowBase = '0 2px 8px'
const maxValue = 50
if (scrollTop >= 0) {
const input = scrollTop > maxValue ? maxValue : scrollTop
const boxShadowOpacity = mapRange(input, 0, maxValue, 0.0, 0.16)
const borderOpacity = mapRange(input, 0, maxValue, 0.0, 0.24)
header.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, ${boxShadowOpacity})`
header.style.borderBottomColor = `rgba(0, 0, 0, ${borderOpacity})`
}
}
function manipulateFooterShadow(
footer: HTMLDivElement,
scrollTop: number,
scrollHeight: number,
clientHeight: number
) {
const boxShadowBase = '0 -2px 8px'
const minValue = scrollHeight - 200
const currentScroll = scrollTop + clientHeight
if (currentScroll >= minValue) {
const input = currentScroll < minValue ? minValue : currentScroll
const boxShadowOpacity = mapRange(
input,
minValue,
scrollHeight,
0.16,
0.0
)
const borderOpacity = mapRange(input, minValue, scrollHeight, 0.24, 0.0)
footer.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, ${boxShadowOpacity})`
footer.style.borderTopColor = `rgba(0, 0, 0, ${borderOpacity})`
}
}
const calculateFooterShadow = debounce(() => {
const boxShadowBase = '0 -2px 8px'
const scrollable = document.querySelector(`.${styles.scrollValue}`)
const footer = footerRef
if (footer && footer.current) {
if (scrollable && scrollable.clientHeight >= scrollable.scrollHeight) {
footer.current.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, 0)`
footer.current.style.borderTopColor = `rgba(0, 0, 0, 0)`
} else {
footer.current.style.boxShadow = `${boxShadowBase} rgba(0, 0, 0, 0.16)`
footer.current.style.borderTopColor = `rgba(0, 0, 0, 0.24)`
}
}
}, 100)
useEffect(() => {
window.addEventListener('resize', calculateFooterShadow)
calculateFooterShadow()
return () => {
window.removeEventListener('resize', calculateFooterShadow)
}
}, [calculateFooterShadow])
function mapRange(
value: number,
low1: number,
high1: number,
low2: number,
high2: number
) {
return low2 + ((high2 - low2) * (value - low1)) / (high1 - low1)
}
// Methods: Data methods // Methods: Data methods
function persistFromState() { function persistFromState() {
if (!party) return if (!party) return
@ -189,7 +285,7 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
} }
function updateDetails(event: React.MouseEvent) { function updateDetails(event: React.MouseEvent) {
const descriptionValue = descriptionInput.current?.value const descriptionValue = descriptionInput.current?.innerHTML
const details: DetailsObject = { const details: DetailsObject = {
fullAuto: fullAuto, fullAuto: fullAuto,
autoGuard: autoGuard, autoGuard: autoGuard,
@ -210,8 +306,8 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
} }
// Methods: Rendering methods // Methods: Rendering methods
const segmentedControl = () => { const segmentedControl = (
return ( <nav className={styles.segmentedControlWrapper} ref={topContainerRef}>
<SegmentedControl blended={true}> <SegmentedControl blended={true}>
<Segment <Segment
groupName="edit_nav" groupName="edit_nav"
@ -232,66 +328,56 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
{t('modals.edit_team.segments.properties')} {t('modals.edit_team.segments.properties')}
</Segment> </Segment>
</SegmentedControl> </SegmentedControl>
</nav>
) )
}
const nameField = () => { const nameField = (
return ( <Input
<CharLimitedFieldset name="name"
className="Bound"
fieldName="name"
placeholder="Name your team" placeholder="Name your team"
autoFocus={true}
value={name} value={name}
limit={50} maxLength={50}
bound={true}
showCounter={true}
onChange={handleInputChange} onChange={handleInputChange}
error={errors.name}
/> />
) )
}
const raidField = () => { const raidField = (
return (
<RaidCombobox <RaidCombobox
showAllRaidsOption={false} showAllRaidsOption={false}
currentRaid={raid} currentRaid={raid}
onChange={receiveRaid} onChange={receiveRaid}
/> />
) )
}
const extraNotice = () => { const extraNotice = () => {
if (extra) { if (extra) {
return ( return (
<div className="ExtraNotice"> <div className={styles.extraNotice}>
<span className="ExtraNoticeText"> <p>
{raid && raid.group.guidebooks {raid && raid.group.guidebooks
? t('modals.edit_team.extra_notice_guidebooks') ? t('modals.edit_team.extra_notice_guidebooks')
: t('modals.edit_team.extra_notice')} : t('modals.edit_team.extra_notice')}
</span> </p>
</div> </div>
) )
} }
} }
const descriptionField = () => { const descriptionField = (
return ( <Textarea
<div className="DescriptionField"> className="editParty"
<textarea bound={true}
className="Input Bound" placeholder={t('modals.edit_team.placeholders.description')}
name="description" value={description}
placeholder={ onInput={handleTextAreaChanged}
'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} ref={descriptionInput}
defaultValue={description}
/> />
</div>
) )
}
const chargeAttackField = () => { const chargeAttackField = (
return (
<SwitchTableField <SwitchTableField
name="charge_attack" name="charge_attack"
label={t('modals.edit_team.labels.charge_attack')} label={t('modals.edit_team.labels.charge_attack')}
@ -299,10 +385,8 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
onValueChange={handleChargeAttackChanged} onValueChange={handleChargeAttackChanged}
/> />
) )
}
const fullAutoField = () => { const fullAutoField = (
return (
<SwitchTableField <SwitchTableField
name="full_auto" name="full_auto"
label={t('modals.edit_team.labels.full_auto')} label={t('modals.edit_team.labels.full_auto')}
@ -310,10 +394,8 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
onValueChange={handleFullAutoChanged} onValueChange={handleFullAutoChanged}
/> />
) )
}
const autoGuardField = () => { const autoGuardField = (
return (
<SwitchTableField <SwitchTableField
name="auto_guard" name="auto_guard"
label={t('modals.edit_team.labels.auto_guard')} label={t('modals.edit_team.labels.auto_guard')}
@ -321,10 +403,8 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
onValueChange={handleAutoGuardChanged} onValueChange={handleAutoGuardChanged}
/> />
) )
}
const autoSummonField = () => { const autoSummonField = (
return (
<SwitchTableField <SwitchTableField
name="auto_summon" name="auto_summon"
label={t('modals.edit_team.labels.auto_summon')} label={t('modals.edit_team.labels.auto_summon')}
@ -332,10 +412,8 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
onValueChange={handleAutoSummonChanged} onValueChange={handleAutoSummonChanged}
/> />
) )
}
const extraField = () => { const extraField = (
return (
<SwitchTableField <SwitchTableField
name="extra" name="extra"
className="Extra" className="Extra"
@ -346,30 +424,25 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
onValueChange={handleExtraChanged} onValueChange={handleExtraChanged}
/> />
) )
}
const clearTimeField = () => { const clearTimeField = (
return (
<TableField <TableField
className="Numeric"
name="clear_time" name="clear_time"
label={t('modals.edit_team.labels.clear_time')} label={t('modals.edit_team.labels.clear_time')}
> >
<DurationInput <DurationInput
name="clear_time" name="clear_time"
className="Bound" bound={true}
value={clearTime} value={clearTime}
onValueChange={(value: number) => handleClearTimeChanged(value)} onValueChange={(value: number) => handleClearTimeChanged(value)}
/> />
</TableField> </TableField>
) )
}
const turnCountField = () => { const turnCountField = (
return (
<InputTableField <InputTableField
name="turn_count" name="turn_count"
className="Numeric" className="number"
label={t('modals.edit_team.labels.turn_count')} label={t('modals.edit_team.labels.turn_count')}
placeholder="0" placeholder="0"
type="number" type="number"
@ -377,13 +450,11 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
onValueChange={handleTurnCountChanged} onValueChange={handleTurnCountChanged}
/> />
) )
}
const buttonCountField = () => { const buttonCountField = (
return (
<InputTableField <InputTableField
name="button_count" name="button_count"
className="Numeric" className="number"
label={t('modals.edit_team.labels.button_count')} label={t('modals.edit_team.labels.button_count')}
placeholder="0" placeholder="0"
type="number" type="number"
@ -391,13 +462,11 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
onValueChange={handleButtonCountChanged} onValueChange={handleButtonCountChanged}
/> />
) )
}
const chainCountField = () => { const chainCountField = (
return (
<InputTableField <InputTableField
name="chain_count" name="chain_count"
className="Numeric" className="number"
label={t('modals.edit_team.labels.chain_count')} label={t('modals.edit_team.labels.chain_count')}
placeholder="0" placeholder="0"
type="number" type="number"
@ -405,81 +474,68 @@ const EditPartyModal = ({ updateCallback, ...props }: Props) => {
onValueChange={handleChainCountChanged} onValueChange={handleChainCountChanged}
/> />
) )
}
const infoPage = () => { const infoPage = (
return (
<> <>
{nameField()} {nameField}
{raidField()} {raidField}
{extraNotice()} {extraNotice()}
{descriptionField()} {descriptionField}
</> </>
) )
}
const propertiesPage = () => { const propertiesPage = (
return (
<> <>
{chargeAttackField()} {chargeAttackField}
{fullAutoField()} {fullAutoField}
{autoSummonField()} {autoSummonField}
{autoGuardField()} {autoGuardField}
{extraField()} {extraField}
{clearTimeField()} {clearTimeField}
{turnCountField()} {turnCountField}
{buttonCountField()} {buttonCountField}
{chainCountField()} {chainCountField}
</> </>
) )
}
return ( return (
<Dialog open={open} onOpenChange={openChange}> <Dialog open={open} onOpenChange={openChange}>
<DialogTrigger asChild>{props.children}</DialogTrigger> <DialogTrigger asChild>{props.children}</DialogTrigger>
<DialogContent <DialogContent
className="EditTeam" className="editParty"
headerref={headerRef} headerref={topContainerRef}
footerref={footerRef} footerref={footerRef}
onEscapeKeyDown={onEscapeKeyDown} onEscapeKeyDown={onEscapeKeyDown}
onOpenAutoFocus={onOpenAutoFocus} onOpenAutoFocus={onOpenAutoFocus}
> >
<div className="DialogHeader" ref={headerRef}> <DialogHeader title={t('modals.edit_team.title')} ref={headerRef} />
<div className="DialogTop">
<DialogTitle className="DialogTitle"> <div className={styles.content}>
{t('modals.edit_team.title')} {segmentedControl}
</DialogTitle> <div className={fieldsClasses} onScroll={handleScroll}>
{currentSegment === 0 && infoPage}
{currentSegment === 1 && propertiesPage}
</div> </div>
<DialogClose className="DialogClose" asChild>
<span>
<CrossIcon />
</span>
</DialogClose>
</div> </div>
<div className="Content"> <DialogFooter
{segmentedControl()} ref={footerRef}
<div className="Fields"> rightElements={[
{currentSegment === 0 && infoPage()}
{currentSegment === 1 && propertiesPage()}
</div>
</div>
<div className="DialogFooter" ref={footerRef}>
<div className="Left"></div>
<div className="Right Buttons Spaced">
<Button <Button
contained={true} bound={true}
key="cancel"
text={t('buttons.cancel')} text={t('buttons.cancel')}
onClick={openChange} onClick={openChange}
/> />,
<Button <Button
contained={true} bound={true}
key="confirm"
rightAccessoryIcon={<CheckIcon />} rightAccessoryIcon={<CheckIcon />}
text={t('modals.edit_team.buttons.confirm')} text={t('modals.edit_team.buttons.confirm')}
onClick={updateDetails} onClick={updateDetails}
/>,
]}
/> />
</div>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
) )