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
This commit is contained in:
Justin Edmund 2023-06-18 01:29:53 -07:00 committed by GitHub
parent 938e34f21c
commit 835cdfff6f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
35 changed files with 853 additions and 581 deletions

View file

@ -414,7 +414,8 @@ const FilterModal = (props: Props) => {
{originalOnlyField()}
</div>
<div className="DialogFooter" ref={footerRef}>
<div className="Buttons Spaced">
<div className="Left"></div>
<div className="Right Buttons Spaced">
<Button
blended={true}
text={t('modals.filters.buttons.clear')}

View file

@ -330,10 +330,13 @@ const AccountModal = React.forwardRef<HTMLDivElement, Props>(
{themeField()}
</div>
<div className="DialogFooter" ref={footerRef}>
<Button
contained={true}
text={t('modals.settings.buttons.confirm')}
/>
<div className="Left"></div>
<div className="Right">
<Button
contained={true}
text={t('modals.settings.buttons.confirm')}
/>
</div>
</div>
</form>
</DialogContent>

View file

@ -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<HTMLInputElement> {
fieldName: string
placeholder: string
value?: string
@ -11,47 +18,61 @@ interface Props {
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
}
const CharLimitedFieldset = React.forwardRef<HTMLInputElement, Props>(
function useFieldSet(props, ref) {
const fieldType = ['password', 'confirm_password'].includes(props.fieldName)
? 'password'
: 'text'
const CharLimitedFieldset: ForwardRefRenderFunction<HTMLInputElement, Props> = (
{
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<HTMLInputElement>) {
setCurrentCount(props.limit - event.currentTarget.value.length)
if (props.onChange) props.onChange(event)
// Event handlers
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const { value: inputValue } = event.currentTarget
setCurrentCount(limit - inputValue.length)
if (onInputChange) {
onInputChange(event)
}
return (
<fieldset className="Fieldset">
<div className="Joined">
<input
autoComplete="off"
className="Input"
type={fieldType}
name={props.fieldName}
placeholder={props.placeholder}
defaultValue={props.value || ''}
onBlur={props.onBlur}
onChange={onChange}
maxLength={props.limit}
ref={ref}
formNoValidate
/>
<span className="Counter">{currentCount}</span>
</div>
{props.error.length > 0 && <p className="InputError">{props.error}</p>}
</fieldset>
)
}
)
export default CharLimitedFieldset
// Rendering methods
return (
<fieldset className="Fieldset">
<div className={classNames({ Joined: true }, props.className)}>
<input
{...props}
autoComplete="off"
className="Input"
type={props.type}
name={fieldName}
placeholder={placeholder}
defaultValue={value || ''}
onBlur={onBlur}
onChange={handleInputChange}
maxLength={limit}
ref={ref}
formNoValidate
/>
<span className="Counter">{currentCount}</span>
</div>
{error.length > 0 && <p className="InputError">{error}</p>}
</fieldset>
)
}
export default forwardRef(CharLimitedFieldset)

View file

@ -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%;
}
}
}

View file

@ -14,7 +14,10 @@ interface Props
}
const DurationInput = React.forwardRef<HTMLInputElement, Props>(
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<HTMLInputElement, Props>(
},
className
)}
value={`${getSeconds()}`.padStart(2, '0')}
value={getSeconds() > 0 ? `${getSeconds()}`.padStart(2, '0') : ''}
onChange={handleSecondsChange}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}

View file

@ -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 */

View file

@ -1,4 +1,3 @@
.InputField.TableField .Input {
.InputField.TableField .Input[type='number'] {
text-align: right;
width: $unit-8x;
}

View file

@ -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>,
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<HTMLInputElement>) {
setValue(parseInt(event.currentTarget?.value))
setInputValue(`${parseInt(event.currentTarget?.value)}`)
}
return (
<TableField
name={props.name}
className="InputField"
imageAlt={props.imageAlt}
imageClass={props.imageClass}
imageSrc={props.imageSrc}
label={props.label}
{...props}
name={props.name || ''}
className={classNames({ InputField: true }, props.className)}
imageAlt={imageAlt}
imageClass={imageClass}
imageSrc={imageSrc}
label={label}
>
<Input
className="Bound"
placeholder={props.placeholder}
type="number"
value={value ? `${value}` : ''}
value={inputValue ? `${inputValue}` : ''}
step={1}
tabIndex={props.tabIndex}
type={props.type}
onChange={onInputChange}
/>
</TableField>

View file

@ -1,7 +1,7 @@
.Overlay {
isolation: isolate;
position: fixed;
z-index: 30;
z-index: 9;
top: 0;
right: 0;
bottom: 0;

View file

@ -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<HTMLDivElement, Props>(function Popover(
<PopoverPrimitive.Trigger
className={triggerClasses}
data-placeholder={!props.value}
tabIndex={props.triggerTabIndex}
>
{icon}
{value}

View file

@ -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);

View file

@ -0,0 +1,3 @@
.SelectField.TableField .Right {
justify-content: flex-end;
}

View file

@ -31,6 +31,7 @@ const SelectTableField = (props: Props) => {
return (
<TableField
name={props.name}
className="SelectField"
imageAlt={props.imageAlt}
imageClass={props.imageClass}
imageSrc={props.imageSrc}

View file

@ -5,4 +5,8 @@
text-align: right;
width: $unit-8x;
}
.Right {
justify-content: flex-end;
}
}

View file

@ -36,6 +36,7 @@ const Switch = (props: Props) => {
disabled={disabled}
required={required}
value={value}
tabIndex={props.tabIndex}
onCheckedChange={onCheckedChange}
>
<RadixSwitch.Thumb className={thumbClasses} />

View file

@ -0,0 +1,9 @@
.TableField.SwitchTableField {
&.Extra .Switch[data-state='checked'] {
background: var(--extra-purple-secondary);
}
.Right {
justify-content: end;
}
}

View file

@ -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<HTMLDivElement> {
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 (
<TableField
name={props.name}
className="SwitchField"
description={props.description}
className={classes}
imageAlt={props.imageAlt}
imageClass={props.imageClass}
imageSrc={props.imageSrc}
@ -43,6 +55,8 @@ const SwitchTableField = (props: Props) => {
<Switch
name={props.name}
checked={value}
disabled={props.disabled}
tabIndex={props.tabIndex}
onCheckedChange={onValueChange}
/>
</TableField>

View file

@ -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%;

View file

@ -32,7 +32,7 @@ const TableField = (props: Props) => {
<div className="Left">
<div className="Info">
<h3>{props.label}</h3>
<p>{props.description}</p>
{props.description && <p>{props.description}</p>}
</div>
<div className="Image">{image()}</div>
</div>

View file

@ -11,12 +11,10 @@ import classNames from 'classnames'
// Props
interface Props {
grid: GridArray<GridWeapon>
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 = (
<ul id="ExtraWeaponGrid">
{Array.from(Array(EXTRA_WEAPONS_COUNT)).map((x, i) => {
const itemClasses = classNames({
@ -77,17 +65,8 @@ const ExtraWeaponsGrid = ({
<div className={classes}>
<div className="Header">
<h3>{t('extra_weapons')}</h3>
{editable ? (
<Switch
name="ExtraWeapons"
checked={enabled}
onCheckedChange={onCheckedChange}
/>
) : (
''
)}
</div>
{enabled ? enabledElement : ''}
{extraWeapons}
</div>
)
}

View file

@ -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 = (
<ul id="GuidebooksGrid">
{Array.from(Array(EXTRA_WEAPONS_COUNT)).map((x, i) => {
const itemClasses = classNames({
@ -75,21 +58,12 @@ const GuidebooksGrid = ({
<div className={classes}>
<div className="Header">
<h3>{t('sephira_guidebooks')}</h3>
{editable ? (
<Switch
name="Guidebooks"
checked={enabled}
onCheckedChange={onCheckedChange}
/>
) : (
''
)}
</div>
{enabled ? enabledElement : ''}
{guidebooks}
</div>
)
return editable || (enabled && !editable) ? guidebookElement : <div />
return guidebookElement
}
export default GuidebooksGrid

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,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<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

View file

@ -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('')
}

View file

@ -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<HTMLTextAreaElement>()
const [open, setOpen] = useState(false)
const [alertOpen, setAlertOpen] = useState(false)
const [remixes, setRemixes] = useState<Party[]>([])
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<HTMLTextAreaElement>) {
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 (
<section className={editableClasses}>
<TextFieldset
fieldName="name"
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={props.party?.description}
onChange={handleTextAreaChange}
error={errors.description}
ref={descriptionInput}
/>
<div className="bottom">
<div className="left">
{router.pathname !== '/new' ? (
<Button
leftAccessoryIcon={<CrossIcon />}
className="Blended medium destructive"
onClick={handleClick}
text={t('buttons.delete')}
/>
) : (
''
)}
</div>
<div className="right">
<Button text={t('buttons.cancel')} onClick={toggleDetails} />
<Button
leftAccessoryIcon={<CheckIcon className="Check" />}
text={t('buttons.save_info')}
onClick={updateDetails}
/>
</div>
</div>
</section>
)
}
const readOnly = () => {
return (
<section className={readOnlyClasses}>
@ -291,10 +192,7 @@ const PartyDetails = (props: Props) => {
return (
<>
<section className="DetailsWrapper">
{readOnly()}
{editable()}
</section>
<section className="DetailsWrapper">{readOnly()}</section>
{remixes && remixes.length > 0 ? remixSection() : ''}
</>
)

View file

@ -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<HTMLInputElement>()
const descriptionInput = React.createRef<HTMLTextAreaElement>()
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<number | undefined>(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<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 handleClearTimeInput(value: number) {
if (!isNaN(value)) setClearTime(value)
}
function handleTurnCountInput(event: React.ChangeEvent<HTMLInputElement>) {
const value = parseInt(event.currentTarget.value)
if (!isNaN(value)) setTurnCount(value)
}
function handleButtonCountInput(event: ChangeEvent<HTMLInputElement>) {
const value = parseInt(event.currentTarget.value)
if (!isNaN(value)) setButtonCount(value)
}
function handleChainCountInput(event: ChangeEvent<HTMLInputElement>) {
const value = parseInt(event.currentTarget.value)
if (!isNaN(value)) setChainCount(value)
}
function handleInputKeyDown(event: KeyboardEvent<HTMLInputElement>) {
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) => {
</Tooltip>
)
}
const editable = () => {
return (
<section className={editableClasses}>
<CharLimitedFieldset
fieldName="name"
placeholder="Name your team"
value={props.party?.name}
limit={50}
onChange={handleInputChange}
error={errors.name}
ref={nameInput}
/>
<RaidCombobox
showAllRaidsOption={false}
currentRaid={props.party?.raid ? props.party?.raid : undefined}
onChange={receiveRaid}
/>
<ul className="SwitchToggleGroup DetailToggleGroup">
<li className="Ougi ToggleSection">
<label htmlFor="ougi">
<span>{t('party.details.labels.charge_attack')}</span>
<div>
<Switch
name="charge_attack"
onCheckedChange={handleChargeAttackChanged}
value={switchValue(chargeAttack)}
checked={chargeAttack}
/>
</div>
</label>
</li>
<li className="FullAuto ToggleSection">
<label htmlFor="full_auto">
<span>{t('party.details.labels.full_auto')}</span>
<div>
<Switch
onCheckedChange={handleFullAutoChanged}
name="full_auto"
value={switchValue(fullAuto)}
checked={fullAuto}
/>
</div>
</label>
</li>
<li className="AutoGuard ToggleSection">
<label htmlFor="auto_guard">
<span>{t('party.details.labels.auto_guard')}</span>
<div>
<Switch
onCheckedChange={handleAutoGuardChanged}
name="auto_guard"
value={switchValue(autoGuard)}
disabled={!fullAuto}
checked={autoGuard}
/>
</div>
</label>
</li>
</ul>
<ul className="InputToggleGroup DetailToggleGroup">
<li className="InputSection">
<label htmlFor="auto_guard">
<span>{t('party.details.labels.button_chain')}</span>
<div className="Input Bound">
<Input
name="buttons"
type="number"
placeholder="0"
value={`${buttonCount}`}
min="0"
max="99"
onChange={handleButtonCountInput}
onKeyDown={handleInputKeyDown}
/>
<span>b</span>
<Input
name="chains"
type="number"
placeholder="0"
min="0"
max="99"
value={`${chainCount}`}
onChange={handleChainCountInput}
onKeyDown={handleInputKeyDown}
/>
<span>c</span>
</div>
</label>
</li>
<li className="InputSection">
<label htmlFor="auto_guard">
<span>{t('party.details.labels.turn_count')}</span>
<Input
name="turn_count"
className="AlignRight Bound"
type="number"
step="1"
min="1"
max="999"
placeholder="0"
value={`${turnCount}`}
onChange={handleTurnCountInput}
onKeyDown={handleInputKeyDown}
/>
</label>
</li>
<li className="InputSection">
<label htmlFor="auto_guard">
<span>{t('party.details.labels.clear_time')}</span>
<div>
<DurationInput
name="clear_time"
className="Bound"
placeholder="00:00"
value={clearTime}
onValueChange={(value: number) => handleClearTimeInput(value)}
/>
</div>
</label>
</li>
</ul>
<div className="bottom">
<div className="right">
<Button text={t('buttons.cancel')} onClick={toggleDetails} />
<Button
leftAccessoryIcon={<CheckIcon className="Check" />}
text={t('buttons.save_info')}
onClick={updateDetails}
/>
</div>
</div>
</section>
)
}
const readOnly = () => {
return <section className={readOnlyClasses}>{renderTokens()}</section>
}
return (
<>
@ -666,11 +375,15 @@ const PartyHeader = (props: Props) => {
</div>
{party.editable ? (
<div className="Right">
<Button
leftAccessoryIcon={<EditIcon />}
text={t('buttons.show_info')}
onClick={toggleDetails}
/>
<EditPartyModal
party={props.party}
updateCallback={props.updateCallback}
>
<Button
leftAccessoryIcon={<EditIcon />}
text={t('buttons.show_info')}
/>
</EditPartyModal>
<PartyDropdown
editable={props.editable}
deleteTeamCallback={props.deleteCallback}
@ -684,8 +397,7 @@ const PartyHeader = (props: Props) => {
</div>
)}
</div>
{readOnly()}
{editable()}
<section className={classes}>{renderTokens()}</section>
</section>
</>
)

View file

@ -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;

View file

@ -18,6 +18,7 @@ interface Props {
currentRaid?: Raid
defaultRaid?: Raid
minimal?: boolean
tabIndex?: number
onChange?: (raid?: Raid) => void
onBlur?: (event: React.ChangeEvent<HTMLSelectElement>) => 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()}
>
<Command className="Raid Combobox">

View file

@ -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;
}

View file

@ -377,23 +377,24 @@ const WeaponGrid = (props: Props) => {
const extraElement = (
<ExtraContainer>
<ExtraWeaponsGrid
grid={appState.grid.weapons.allWeapons}
enabled={appState.party.extra}
editable={props.editable}
offset={numWeapons}
removeWeapon={removeWeapon}
updateExtra={props.updateExtra}
updateObject={receiveWeaponFromSearch}
updateUncap={initiateUncapUpdate}
/>
<GuidebooksGrid
grid={appState.party.guidebooks}
editable={props.editable}
offset={numWeapons}
removeGuidebook={removeGuidebook}
updateObject={receiveGuidebookFromSearch}
/>
{appState.party.raid && appState.party.raid.group.extra && (
<ExtraWeaponsGrid
grid={appState.grid.weapons.allWeapons}
editable={props.editable}
offset={numWeapons}
removeWeapon={removeWeapon}
updateObject={receiveWeaponFromSearch}
updateUncap={initiateUncapUpdate}
/>
)}
{appState.party.raid && appState.party.raid.group.guidebooks && (
<GuidebooksGrid
grid={appState.party.guidebooks}
editable={props.editable}
removeGuidebook={removeGuidebook}
updateObject={receiveGuidebookFromSearch}
/>
)}
</ExtraContainer>
)

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": "編成を削除する操作は取り消せません。",

1
types/Party.d.ts vendored
View file

@ -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

View file

@ -10,5 +10,6 @@ interface RaidGroup {
section: number
order: number
extra: boolean
guidebooks: boolean
hl: boolean
}

1
types/index.d.ts vendored
View file

@ -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