diff --git a/components/FilterModal/index.tsx b/components/FilterModal/index.tsx index 7eaec126..ed41db21 100644 --- a/components/FilterModal/index.tsx +++ b/components/FilterModal/index.tsx @@ -414,7 +414,8 @@ const FilterModal = (props: Props) => { {originalOnlyField()}
-
+
+
-
diff --git a/components/common/CharLimitedFieldset/index.tsx b/components/common/CharLimitedFieldset/index.tsx index 16cbef3e..d3aa498c 100644 --- a/components/common/CharLimitedFieldset/index.tsx +++ b/components/common/CharLimitedFieldset/index.tsx @@ -1,7 +1,14 @@ -import React, { useEffect, useState } from 'react' +import React, { + ForwardRefRenderFunction, + forwardRef, + useEffect, + useState, +} from 'react' + +import classNames from 'classnames' import './index.scss' -interface Props { +interface Props extends React.HTMLProps { fieldName: string placeholder: string value?: string @@ -11,47 +18,61 @@ interface Props { onChange?: (event: React.ChangeEvent) => void } -const CharLimitedFieldset = React.forwardRef( - function useFieldSet(props, ref) { - const fieldType = ['password', 'confirm_password'].includes(props.fieldName) - ? 'password' - : 'text' +const CharLimitedFieldset: ForwardRefRenderFunction = ( + { + fieldName, + placeholder, + value, + limit, + error, + onBlur, + onChange: onInputChange, + ...props + }, + ref +) => { + // States + const [currentCount, setCurrentCount] = useState( + () => limit - (value || '').length + ) - const [currentCount, setCurrentCount] = useState(0) + // Hooks + useEffect(() => { + setCurrentCount(limit - (value || '').length) + }, [limit, value]) - useEffect(() => { - setCurrentCount( - props.value ? props.limit - props.value.length : props.limit - ) - }, [props.limit, props.value]) - - function onChange(event: React.ChangeEvent) { - setCurrentCount(props.limit - event.currentTarget.value.length) - if (props.onChange) props.onChange(event) + // Event handlers + const handleInputChange = (event: React.ChangeEvent) => { + const { value: inputValue } = event.currentTarget + setCurrentCount(limit - inputValue.length) + if (onInputChange) { + onInputChange(event) } - - return ( -
-
- - {currentCount} -
- {props.error.length > 0 &&

{props.error}

} -
- ) } -) -export default CharLimitedFieldset + // Rendering methods + return ( +
+
+ + {currentCount} +
+ {error.length > 0 &&

{error}

} +
+ ) +} + +export default forwardRef(CharLimitedFieldset) diff --git a/components/common/DialogContent/index.scss b/components/common/DialogContent/index.scss index 648441a0..7d1a311c 100644 --- a/components/common/DialogContent/index.scss +++ b/components/common/DialogContent/index.scss @@ -11,7 +11,7 @@ min-width: 100vw; overflow-y: auto; color: inherit; - z-index: 40; + z-index: 10; .DialogContent { $multiplier: 4; @@ -160,7 +160,8 @@ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.16); border-top: 1px solid rgba(0, 0, 0, 0.24); display: flex; - flex-direction: column; + flex-direction: row; + justify-content: space-between; padding: ($unit * 1.5) ($unit * $multiplier) $unit-3x; position: sticky; @@ -178,7 +179,6 @@ &.Spaced { justify-content: space-between; - width: 100%; } } } diff --git a/components/common/DurationInput/index.tsx b/components/common/DurationInput/index.tsx index 91513838..42f11c1b 100644 --- a/components/common/DurationInput/index.tsx +++ b/components/common/DurationInput/index.tsx @@ -14,7 +14,10 @@ interface Props } const DurationInput = React.forwardRef( - function DurationInput({ className, value, onValueChange }, forwardedRef) { + function DurationInput( + { className, value, onValueChange, ...props }, + forwardedRef + ) { // State const [duration, setDuration] = useState('') const [minutesSelected, setMinutesSelected] = useState(false) @@ -202,7 +205,7 @@ const DurationInput = React.forwardRef( }, className )} - value={`${getSeconds()}`.padStart(2, '0')} + value={getSeconds() > 0 ? `${getSeconds()}`.padStart(2, '0') : ''} onChange={handleSecondsChange} onKeyUp={handleKeyUp} onKeyDown={handleKeyDown} diff --git a/components/common/Input/index.scss b/components/common/Input/index.scss index 897a2779..75a864fd 100644 --- a/components/common/Input/index.scss +++ b/components/common/Input/index.scss @@ -23,6 +23,11 @@ &:hover { background-color: var(--input-bound-bg-hover); } + + &::placeholder { + /* Chrome, Firefox, Opera, Safari 10.1+ */ + color: var(--text-tertiary) !important; + } } &.AlignRight { @@ -43,7 +48,7 @@ width: 0; } -::placeholder { +.Input::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ color: var(--text-secondary) !important; opacity: 1; /* Firefox */ diff --git a/components/common/InputTableField/index.scss b/components/common/InputTableField/index.scss index 94544da9..e232dbfb 100644 --- a/components/common/InputTableField/index.scss +++ b/components/common/InputTableField/index.scss @@ -1,4 +1,3 @@ -.InputField.TableField .Input { +.InputField.TableField .Input[type='number'] { text-align: right; - width: $unit-8x; } diff --git a/components/common/InputTableField/index.tsx b/components/common/InputTableField/index.tsx index 26fdac25..a043d7ed 100644 --- a/components/common/InputTableField/index.tsx +++ b/components/common/InputTableField/index.tsx @@ -3,50 +3,60 @@ import Input from '~components/common/Input' import TableField from '~components/common/TableField' import './index.scss' +import classNames from 'classnames' -interface Props { - name: string +interface Props + extends React.DetailedHTMLProps< + React.InputHTMLAttributes, + HTMLInputElement + > { label: string description?: string - placeholder?: string - value?: number - className?: string imageAlt?: string imageClass?: string imageSrc?: string[] - onValueChange: (value: number) => void + onValueChange: (value?: string) => void } -const InputTableField = (props: Props) => { - const [value, setValue] = useState(0) +const InputTableField = ({ + label, + description, + imageAlt, + imageClass, + imageSrc, + ...props +}: Props) => { + const [inputValue, setInputValue] = useState('') useEffect(() => { - if (props.value) setValue(props.value) + if (props.value) setInputValue(`${props.value}`) }, [props.value]) useEffect(() => { - props.onValueChange(value) - }, [value]) + props.onValueChange(inputValue) + }, [inputValue]) function onInputChange(event: React.ChangeEvent) { - setValue(parseInt(event.currentTarget?.value)) + setInputValue(`${parseInt(event.currentTarget?.value)}`) } return ( diff --git a/components/common/Overlay/index.scss b/components/common/Overlay/index.scss index 34d953a6..850f5d8f 100644 --- a/components/common/Overlay/index.scss +++ b/components/common/Overlay/index.scss @@ -1,7 +1,7 @@ .Overlay { isolation: isolate; position: fixed; - z-index: 30; + z-index: 9; top: 0; right: 0; bottom: 0; diff --git a/components/common/Popover/index.tsx b/components/common/Popover/index.tsx index f334b6a7..83265154 100644 --- a/components/common/Popover/index.tsx +++ b/components/common/Popover/index.tsx @@ -23,6 +23,7 @@ interface Props extends ComponentProps<'div'> { className?: string placeholder?: string } + triggerTabIndex?: number value?: { element: ReactNode rawValue: string @@ -83,6 +84,7 @@ const Popover = React.forwardRef(function Popover( {icon} {value} diff --git a/components/common/Select/index.scss b/components/common/Select/index.scss index 170dba71..ddf1fade 100644 --- a/components/common/Select/index.scss +++ b/components/common/Select/index.scss @@ -75,8 +75,8 @@ .Select { background: var(--dialog-bg); border-radius: $card-corner; - border: $hover-stroke; - box-shadow: $hover-shadow; + border: 1px solid rgba(0, 0, 0, 0.24); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.16); padding: 0 $unit; z-index: 40; min-width: var(--radix-select-trigger-width); diff --git a/components/common/SelectTableField/index.scss b/components/common/SelectTableField/index.scss index e69de29b..57f5640d 100644 --- a/components/common/SelectTableField/index.scss +++ b/components/common/SelectTableField/index.scss @@ -0,0 +1,3 @@ +.SelectField.TableField .Right { + justify-content: flex-end; +} diff --git a/components/common/SelectTableField/index.tsx b/components/common/SelectTableField/index.tsx index 2eded956..83e88b0e 100644 --- a/components/common/SelectTableField/index.tsx +++ b/components/common/SelectTableField/index.tsx @@ -31,6 +31,7 @@ const SelectTableField = (props: Props) => { return ( { disabled={disabled} required={required} value={value} + tabIndex={props.tabIndex} onCheckedChange={onCheckedChange} > diff --git a/components/common/SwitchTableField/index.scss b/components/common/SwitchTableField/index.scss index e69de29b..d7b95b2b 100644 --- a/components/common/SwitchTableField/index.scss +++ b/components/common/SwitchTableField/index.scss @@ -0,0 +1,9 @@ +.TableField.SwitchTableField { + &.Extra .Switch[data-state='checked'] { + background: var(--extra-purple-secondary); + } + + .Right { + justify-content: end; + } +} diff --git a/components/common/SwitchTableField/index.tsx b/components/common/SwitchTableField/index.tsx index db9c5352..2344572d 100644 --- a/components/common/SwitchTableField/index.tsx +++ b/components/common/SwitchTableField/index.tsx @@ -1,15 +1,18 @@ import { useEffect, useState } from 'react' +import classNames from 'classnames' import Switch from '~components/common/Switch' import TableField from '~components/common/TableField' import './index.scss' -interface Props { +interface Props extends React.HTMLAttributes { name: string label: string description?: string + disabled?: boolean value?: boolean className?: string + tabIndex?: number imageAlt?: string imageClass?: string imageSrc?: string[] @@ -31,10 +34,19 @@ const SwitchTableField = (props: Props) => { setValue(value) } + const classes = classNames( + { + SwitchTableField: true, + Disabled: props.disabled, + }, + props.className + ) + return ( { diff --git a/components/common/TableField/index.scss b/components/common/TableField/index.scss index ede6eed9..100f9016 100644 --- a/components/common/TableField/index.scss +++ b/components/common/TableField/index.scss @@ -3,6 +3,7 @@ display: grid; gap: $unit-2x; grid-template-columns: 1fr auto; + min-height: $unit-6x; justify-content: space-between; padding: $unit-half 0; width: 100%; @@ -17,7 +18,30 @@ color: var(--accent-blue); } + &.Numeric .Right > .Input, + &.Numeric .Right > .Duration { + text-align: right; + max-width: $unit-12x; + width: $unit-12x; + } + + &.Numeric .Right > .Duration { + justify-content: flex-end; + box-sizing: border-box; + } + + &.Disabled { + &:hover .Left .Info h3 { + color: var(--text-tertiary); + } + + .Left .Info h3 { + color: var(--text-tertiary); + } + } + .Left { + align-items: center; display: flex; flex-direction: row; gap: $unit; @@ -59,7 +83,6 @@ color: var(--text-secondary); font-size: $font-small; line-height: 1.1; - max-width: 300px; &.jp { max-width: 270px; @@ -71,6 +94,7 @@ align-items: center; display: flex; flex-direction: row; + justify-content: flex-end; gap: $unit-2x; width: 100%; diff --git a/components/common/TableField/index.tsx b/components/common/TableField/index.tsx index 3b53aac9..b0d6bb1c 100644 --- a/components/common/TableField/index.tsx +++ b/components/common/TableField/index.tsx @@ -32,7 +32,7 @@ const TableField = (props: Props) => {

{props.label}

-

{props.description}

+ {props.description &&

{props.description}

}
{image()}
diff --git a/components/extra/ExtraWeaponsGrid/index.tsx b/components/extra/ExtraWeaponsGrid/index.tsx index 6dc39c6d..c42cc618 100644 --- a/components/extra/ExtraWeaponsGrid/index.tsx +++ b/components/extra/ExtraWeaponsGrid/index.tsx @@ -11,12 +11,10 @@ import classNames from 'classnames' // Props interface Props { grid: GridArray - enabled: boolean editable: boolean found?: boolean offset: number removeWeapon: (id: string) => void - updateExtra: (enabled: boolean) => void updateObject: (object: SearchableObject, position: number) => void updateUncap: (id: string, position: number, uncap: number) => void } @@ -26,12 +24,9 @@ const EXTRA_WEAPONS_COUNT = 3 const ExtraWeaponsGrid = ({ grid, - enabled, editable, - found, offset, removeWeapon, - updateExtra, updateObject, updateUncap, }: Props) => { @@ -40,16 +35,9 @@ const ExtraWeaponsGrid = ({ const classes = classNames({ ExtraWeapons: true, ContainerItem: true, - Disabled: !enabled, }) - function onCheckedChange(checked: boolean) { - updateExtra(checked) - } - - const disabledElement = <> - - const enabledElement = ( + const extraWeapons = (
    {Array.from(Array(EXTRA_WEAPONS_COUNT)).map((x, i) => { const itemClasses = classNames({ @@ -77,17 +65,8 @@ const ExtraWeaponsGrid = ({

    {t('extra_weapons')}

    - {editable ? ( - - ) : ( - '' - )}
    - {enabled ? enabledElement : ''} + {extraWeapons}
    ) } diff --git a/components/extra/GuidebooksGrid/index.tsx b/components/extra/GuidebooksGrid/index.tsx index 25a8e859..81602925 100644 --- a/components/extra/GuidebooksGrid/index.tsx +++ b/components/extra/GuidebooksGrid/index.tsx @@ -12,7 +12,6 @@ import './index.scss' interface Props { grid: GuidebookList editable: boolean - offset: number removeGuidebook: (position: number) => void updateObject: (object: SearchableObject, position: number) => void } @@ -28,28 +27,12 @@ const GuidebooksGrid = ({ }: Props) => { const { t } = useTranslation('common') - const [enabled, setEnabled] = useState(false) - const classes = classNames({ Guidebooks: true, ContainerItem: true, - Disabled: !enabled, }) - useEffect(() => { - console.log('Grid updated') - if (hasGuidebooks()) setEnabled(true) - }, [grid]) - - function hasGuidebooks() { - return grid && (grid[0] || grid[1] || grid[2]) - } - - function onCheckedChange(checked: boolean) { - setEnabled(checked) - } - - const enabledElement = ( + const guidebooks = (
      {Array.from(Array(EXTRA_WEAPONS_COUNT)).map((x, i) => { const itemClasses = classNames({ @@ -75,21 +58,12 @@ const GuidebooksGrid = ({

      {t('sephira_guidebooks')}

      - {editable ? ( - - ) : ( - '' - )}
      - {enabled ? enabledElement : ''} + {guidebooks}
      ) - return editable || (enabled && !editable) ? guidebookElement :
      + return guidebookElement } export default GuidebooksGrid 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..fb0c9eb6 --- /dev/null +++ b/components/party/EditPartyModal/index.tsx @@ -0,0 +1,477 @@ +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 ( +
      + +
      + ) + } + + const chargeAttackField = () => { + return ( + + ) + } + + const fullAutoField = () => { + return ( + + ) + } + + const autoGuardField = () => { + return ( + + ) + } + + const autoSummonField = () => { + return ( + + ) + } + + const extraField = () => { + return ( + + ) + } + + const clearTimeField = () => { + return ( + + handleClearTimeChanged(value)} + /> + + ) + } + + const turnCountField = () => { + return ( + + ) + } + + const buttonCountField = () => { + return ( + + ) + } + + const chainCountField = () => { + return ( + + ) + } + + const infoPage = () => { + return ( + <> + {nameField()} + {raidField()} + {extraNotice()} + {descriptionField()} + + ) + } + + const propertiesPage = () => { + return ( + <> + {chargeAttackField()} + {fullAutoField()} + {autoSummonField()} + {autoGuardField()} + {extraField()} + {clearTimeField()} + {turnCountField()} + {buttonCountField()} + {chainCountField()} + + ) + } + + return ( + + {props.children} + +
      +
      + + {t('modals.edit_team.title')} + +
      + + + + + +
      + +
      + {segmentedControl()} +
      + {currentSegment === 0 && infoPage()} + {currentSegment === 1 && propertiesPage()} +
      +
      +
      +
      +
      +
      +
      +
      +
      + ) +} + +export default EditPartyModal diff --git a/components/party/Party/index.tsx b/components/party/Party/index.tsx index 6015c2b5..ef0562fb 100644 --- a/components/party/Party/index.tsx +++ b/components/party/Party/index.tsx @@ -119,6 +119,23 @@ const Party = (props: Props) => { .then((response) => storeParty(response.data.party)) } + async function updateParty(details: DetailsObject) { + const payload = formatDetailsObject(details) + + if (props.team && props.team.id) { + return await api.endpoints.parties + .update(props.team.id, payload) + .then((response) => storeParty(response.data.party)) + .catch((error) => { + const data = error.response.data + if (data.errors && Object.keys(data.errors).includes('guidebooks')) { + const message = t('errors.validation.guidebooks') + setErrorMessage(message) + } + }) + } + } + // Methods: Updating the party's details async function updateDetails(details: DetailsObject) { if (!props.team) return await createParty(details) @@ -131,10 +148,10 @@ const Party = (props: Props) => { const mappings: { [key: string]: string } = { name: 'name', description: 'description', - raid: 'raid_id', chargeAttack: 'charge_attack', fullAuto: 'full_auto', autoGuard: 'auto_guard', + autoSummon: 'auto_summon', clearTime: 'clear_time', buttonCount: 'button_count', chainCount: 'chain_count', @@ -152,6 +169,8 @@ const Party = (props: Props) => { } }) + if (details.raid) payload.raid_id = details.raid.id + if (Object.keys(payload).length >= 1) { return { party: payload } } else { @@ -159,23 +178,6 @@ const Party = (props: Props) => { } } - async function updateParty(details: DetailsObject) { - const payload = formatDetailsObject(details) - - if (props.team && props.team.id) { - return await api.endpoints.parties - .update(props.team.id, payload) - .then((response) => storeParty(response.data.party)) - .catch((error) => { - const data = error.response.data - if (data.errors && Object.keys(data.errors).includes('guidebooks')) { - const message = t('errors.validation.guidebooks') - setErrorMessage(message) - } - }) - } - } - function cancelAlert() { setErrorMessage('') } diff --git a/components/party/PartyDetails/index.tsx b/components/party/PartyDetails/index.tsx index 0fd37033..f2039977 100644 --- a/components/party/PartyDetails/index.tsx +++ b/components/party/PartyDetails/index.tsx @@ -1,6 +1,5 @@ import React, { useEffect, useState } from 'react' import { useRouter } from 'next/router' -import { useSnapshot } from 'valtio' import { useTranslation } from 'next-i18next' import clonedeep from 'lodash.clonedeep' @@ -9,23 +8,13 @@ import LiteYouTubeEmbed from 'react-lite-youtube-embed' import classNames from 'classnames' import reactStringReplace from 'react-string-replace' -import Button from '~components/common/Button' import GridRepCollection from '~components/GridRepCollection' import GridRep from '~components/GridRep' -import Tooltip from '~components/common/Tooltip' -import TextFieldset from '~components/common/TextFieldset' import api from '~utils/api' -import { appState, initialAppState } from '~utils/appState' -import { formatTimeAgo } from '~utils/timeAgo' +import { appState } from '~utils/appState' import { youtube } from '~utils/youtube' -import CheckIcon from '~public/icons/Check.svg' -import CrossIcon from '~public/icons/Cross.svg' -import EllipsisIcon from '~public/icons/Ellipsis.svg' -import EditIcon from '~public/icons/Edit.svg' -import RemixIcon from '~public/icons/Remix.svg' - import type { DetailsObject } from 'types' import './index.scss' @@ -45,10 +34,7 @@ const PartyDetails = (props: Props) => { const youtubeUrlRegex = /(?:https:\/\/www\.youtube\.com\/watch\?v=|https:\/\/youtu\.be\/)([\w-]+)/g - const descriptionInput = React.createRef() - const [open, setOpen] = useState(false) - const [alertOpen, setAlertOpen] = useState(false) const [remixes, setRemixes] = useState([]) const [embeddedDescription, setEmbeddedDescription] = @@ -60,17 +46,6 @@ const PartyDetails = (props: Props) => { Visible: !open, }) - const editableClasses = classNames({ - PartyDetails: true, - Editable: true, - Visible: open, - }) - - const [errors, setErrors] = useState<{ [key: string]: string }>({ - name: '', - description: '', - }) - useEffect(() => { // Extract the video IDs from the description if (appState.party.description) { @@ -104,46 +79,12 @@ const PartyDetails = (props: Props) => { } }, [appState.party.description]) - function handleTextAreaChange(event: React.ChangeEvent) { - event.preventDefault() - - const { name, value } = event.target - let newErrors = errors - - setErrors(newErrors) - } - async function fetchYoutubeData(videoId: string) { return await youtube .getVideoById(videoId, { maxResults: 1 }) .then((data) => data.items[0].snippet.localized.title) } - function toggleDetails() { - // Enabling this code will make live updates not work, - // but I'm not sure why it's here, so we're not going to remove it. - - // if (name !== party.name) { - // const resetName = party.name ? party.name : '' - // setName(resetName) - // if (nameInput.current) nameInput.current.value = resetName - // } - setOpen(!open) - } - - function updateDetails(event: React.MouseEvent) { - const details: DetailsObject = { - description: descriptionInput.current?.value, - } - - props.updateCallback(details) - toggleDetails() - } - - function handleClick() { - setAlertOpen(!alertOpen) - } - // Methods: Navigation function goTo(shortcode?: string) { if (shortcode) router.push(`/p/${shortcode}`) @@ -232,46 +173,6 @@ const PartyDetails = (props: Props) => { }) } - const editable = () => { - return ( -
      - - -
      -
      - {router.pathname !== '/new' ? ( -
      -
      -
      -
      -
      - ) - } - const readOnly = () => { return (
      @@ -291,10 +192,7 @@ const PartyDetails = (props: Props) => { return ( <> -
      - {readOnly()} - {editable()} -
      +
      {readOnly()}
      {remixes && remixes.length > 0 ? remixSection() : ''} ) diff --git a/components/party/PartyHeader/index.tsx b/components/party/PartyHeader/index.tsx index 33f806e0..269f1d7a 100644 --- a/components/party/PartyHeader/index.tsx +++ b/components/party/PartyHeader/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, ChangeEvent, KeyboardEvent } from 'react' +import React, { useEffect, useState } from 'react' import Link from 'next/link' import { useRouter } from 'next/router' import { useSnapshot } from 'valtio' @@ -6,21 +6,16 @@ import { useTranslation } from 'next-i18next' import classNames from 'classnames' import Button from '~components/common/Button' -import CharLimitedFieldset from '~components/common/CharLimitedFieldset' -import DurationInput from '~components/common/DurationInput' -import Input from '~components/common/Input' -import RaidCombobox from '~components/raids/RaidCombobox' -import Switch from '~components/common/Switch' import Tooltip from '~components/common/Tooltip' import Token from '~components/common/Token' +import EditPartyModal from '~components/party/EditPartyModal' import PartyDropdown from '~components/party/PartyDropdown' import { accountState } from '~utils/accountState' import { appState, initialAppState } from '~utils/appState' import { formatTimeAgo } from '~utils/timeAgo' -import CheckIcon from '~public/icons/Check.svg' import EditIcon from '~public/icons/Edit.svg' import RemixIcon from '~public/icons/Remix.svg' import SaveIcon from '~public/icons/Save.svg' @@ -41,7 +36,7 @@ interface Props { } const PartyHeader = (props: Props) => { - const { party, raids } = useSnapshot(appState) + const { party } = useSnapshot(appState) const { t } = useTranslation('common') const router = useRouter() @@ -49,12 +44,7 @@ const PartyHeader = (props: Props) => { const { party: partySnapshot } = useSnapshot(appState) - const nameInput = React.createRef() - const descriptionInput = React.createRef() - - const [open, setOpen] = useState(false) const [name, setName] = useState('') - const [alertOpen, setAlertOpen] = useState(false) const [chargeAttack, setChargeAttack] = useState(true) const [fullAuto, setFullAuto] = useState(false) @@ -65,18 +55,9 @@ const PartyHeader = (props: Props) => { const [turnCount, setTurnCount] = useState(undefined) const [clearTime, setClearTime] = useState(0) - const [raidSlug, setRaidSlug] = useState('') - - const readOnlyClasses = classNames({ + const classes = classNames({ PartyDetails: true, ReadOnly: true, - Visible: !open, - }) - - const editableClasses = classNames({ - PartyDetails: true, - Editable: true, - Visible: open, }) const userClass = classNames({ @@ -93,11 +74,6 @@ const PartyHeader = (props: Props) => { light: party && party.element == 6, }) - const [errors, setErrors] = useState<{ [key: string]: string }>({ - name: '', - description: '', - }) - useEffect(() => { if (props.party) { setName(props.party.name) @@ -130,112 +106,6 @@ const PartyHeader = (props: Props) => { }) }, []) - 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 handleClearTimeInput(value: number) { - if (!isNaN(value)) setClearTime(value) - } - - function handleTurnCountInput(event: React.ChangeEvent) { - const value = parseInt(event.currentTarget.value) - if (!isNaN(value)) setTurnCount(value) - } - - function handleButtonCountInput(event: ChangeEvent) { - const value = parseInt(event.currentTarget.value) - if (!isNaN(value)) setButtonCount(value) - } - - function handleChainCountInput(event: ChangeEvent) { - const value = parseInt(event.currentTarget.value) - if (!isNaN(value)) setChainCount(value) - } - - function handleInputKeyDown(event: KeyboardEvent) { - if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) { - // Allow the key to be processed normally - return - } - - // Get the current value - const input = event.currentTarget - let value = event.currentTarget.value - - // Check if the key that was pressed is the backspace key - if (event.key === 'Backspace') { - // Remove the colon if the value is "12:" - if (value.length === 4) { - value = value.slice(0, -1) - } - - // Allow the backspace key to be processed normally - input.value = value - return - } - - // Check if the key that was pressed is the tab key - if (event.key === 'Tab') { - // Allow the tab key to be processed normally - return - } - - // Get the character that was entered and check if it is numeric - const char = parseInt(event.key) - const isNumber = !isNaN(char) - - // Check if the character should be accepted or rejected - const numberValue = parseInt(`${value}${char}`) - const minValue = parseInt(event.currentTarget.min) - const maxValue = parseInt(event.currentTarget.max) - - if (!isNumber || numberValue < minValue || numberValue > maxValue) { - // Reject the character if it isn't a number, - // or if it exceeds the min and max values - event.preventDefault() - } - } - - function toggleDetails() { - // Enabling this code will make live updates not work, - // but I'm not sure why it's here, so we're not going to remove it. - - // if (name !== party.name) { - // const resetName = party.name ? party.name : '' - // setName(resetName) - // if (nameInput.current) nameInput.current.value = resetName - // } - setOpen(!open) - } - - function receiveRaid(raid?: Raid) { - if (raid) setRaidSlug(raid?.slug) - } - - function switchValue(value: boolean) { - if (value) return 'on' - else return 'off' - } - // Actions: Favorites function toggleFavorite() { if (appState.party.favorited) unsaveFavorite() @@ -258,28 +128,6 @@ const PartyHeader = (props: Props) => { else console.error('Failed to unsave team: No party ID') } - function updateDetails(event: React.MouseEvent) { - const descriptionValue = descriptionInput.current?.value - const allRaids = appState.raidGroups.flatMap((group) => group.raids) - const raid = allRaids.find((raid) => raid.slug === raidSlug) - - const details: DetailsObject = { - fullAuto: fullAuto, - autoGuard: autoGuard, - chargeAttack: chargeAttack, - clearTime: clearTime, - buttonCount: buttonCount, - turnCount: turnCount, - chainCount: chainCount, - name: name, - description: descriptionValue, - raid: raid, - } - - props.updateCallback(details) - toggleDetails() - } - // Methods: Navigation function goTo(shortcode?: string) { if (shortcode) router.push(`/p/${shortcode}`) @@ -487,145 +335,6 @@ const PartyHeader = (props: Props) => { ) } - const editable = () => { - return ( -
      - - -
        -
      • - -
      • -
      • - -
      • -
      • - -
      • -
      -
        -
      • - -
      • -
      • - -
      • -
      • - -
      • -
      - -
      -
      -
      -
      -
      - ) - } - - const readOnly = () => { - return
      {renderTokens()}
      - } return ( <> @@ -666,11 +375,15 @@ const PartyHeader = (props: Props) => {
      {party.editable ? (
      -
      )}
- {readOnly()} - {editable()} +
{renderTokens()}
) diff --git a/components/raids/RaidCombobox/index.scss b/components/raids/RaidCombobox/index.scss index a09f4f78..f3c689f3 100644 --- a/components/raids/RaidCombobox/index.scss +++ b/components/raids/RaidCombobox/index.scss @@ -65,7 +65,7 @@ .Raids { border-bottom-left-radius: $card-corner; border-bottom-right-radius: $card-corner; - height: 50vh; + height: 36vh; overflow-y: scroll; padding: 0 $unit; @@ -120,7 +120,9 @@ } } -.DetailsWrapper .PartyDetails.Editable .Raid.SelectTrigger { +.DetailsWrapper .PartyDetails.Editable .Raid.SelectTrigger, +.EditTeam .Raid.SelectTrigger { + background: var(--input-bound-bg); display: flex; padding-top: 10px; padding-bottom: 11px; @@ -139,9 +141,9 @@ } .ExtraIndicator { - background: var(--extra-purple-bg); + background: var(--extra-purple-secondary); border-radius: $full-corner; - color: var(--extra-purple-text); + color: $grey-100; display: flex; font-weight: $bold; font-size: $font-tiny; diff --git a/components/raids/RaidCombobox/index.tsx b/components/raids/RaidCombobox/index.tsx index 095bc249..5cbe7c0d 100644 --- a/components/raids/RaidCombobox/index.tsx +++ b/components/raids/RaidCombobox/index.tsx @@ -18,6 +18,7 @@ interface Props { currentRaid?: Raid defaultRaid?: Raid minimal?: boolean + tabIndex?: number onChange?: (raid?: Raid) => void onBlur?: (event: React.ChangeEvent) => void } @@ -46,6 +47,7 @@ const untitledGroup: RaidGroup = { section: 0, order: 0, extra: false, + guidebooks: false, raids: [], difficulty: 0, hl: false, @@ -114,7 +116,6 @@ const RaidCombobox = (props: Props) => { // Set current raid and section when the current raid changes useEffect(() => { if (props.currentRaid) { - console.log('We are here with a raid') setCurrentRaid(props.currentRaid) setCurrentSection(props.currentRaid.group.section) } @@ -183,7 +184,6 @@ const RaidCombobox = (props: Props) => { const { top: itemTop } = node.getBoundingClientRect() listRef.current.scrollTop = itemTop - listTop - console.log('Focusing node') node.focus() setScrolled(true) } @@ -534,6 +534,7 @@ const RaidCombobox = (props: Props) => { onOpenChange={toggleOpen} placeholder={t('raids.placeholder')} trigger={{ className: 'Raid' }} + triggerTabIndex={props.tabIndex} value={renderTriggerContent()} > diff --git a/components/raids/RaidItem/index.scss b/components/raids/RaidItem/index.scss index 0a6a0292..89a1c8c3 100644 --- a/components/raids/RaidItem/index.scss +++ b/components/raids/RaidItem/index.scss @@ -5,7 +5,7 @@ &:hover { .ExtraIndicator { - background: var(--extra-purple-primary); + background: var(--extra-purple-secondary); color: white; } @@ -15,6 +15,11 @@ } } + &.Selected .ExtraIndicator { + background: var(--extra-purple-secondary); + color: white; + } + .Text { flex-grow: 1; } diff --git a/components/weapon/WeaponGrid/index.tsx b/components/weapon/WeaponGrid/index.tsx index 44133528..6e1588e3 100644 --- a/components/weapon/WeaponGrid/index.tsx +++ b/components/weapon/WeaponGrid/index.tsx @@ -377,23 +377,24 @@ const WeaponGrid = (props: Props) => { const extraElement = ( - - + {appState.party.raid && appState.party.raid.group.extra && ( + + )} + {appState.party.raid && appState.party.raid.group.guidebooks && ( + + )} ) diff --git a/public/locales/en/common.json b/public/locales/en/common.json index 2ce2bf8b..195c05ec 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -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?", diff --git a/public/locales/ja/common.json b/public/locales/ja/common.json index 9b262c24..3c6c8fe2 100644 --- a/public/locales/ja/common.json +++ b/public/locales/ja/common.json @@ -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": "編成を削除する操作は取り消せません。", diff --git a/types/Party.d.ts b/types/Party.d.ts index 4e86c21e..e2b4d23c 100644 --- a/types/Party.d.ts +++ b/types/Party.d.ts @@ -20,6 +20,7 @@ interface Party { raid: Raid full_auto: boolean auto_guard: boolean + auto_summon: boolean charge_attack: boolean clear_time: number button_count?: number diff --git a/types/RaidGroup.d.ts b/types/RaidGroup.d.ts index d6290a3f..f4dc72f9 100644 --- a/types/RaidGroup.d.ts +++ b/types/RaidGroup.d.ts @@ -10,5 +10,6 @@ interface RaidGroup { section: number order: number extra: boolean + guidebooks: boolean hl: boolean } diff --git a/types/index.d.ts b/types/index.d.ts index 77a721b2..97a81308 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -29,6 +29,7 @@ export type DetailsObject = { [key: string]: boolean | number | string | string[] | Raid | undefined fullAuto?: boolean autoGuard?: boolean + autoSummon?: boolean chargeAttack?: boolean clearTime?: number buttonCount?: number