Deploy quality fixes (#328)

* Update the updates page with new items (#306)

* Add Nier and Estarriola uncaps (#308)

* Update the updates page with new items (#306) (#307)

* Update .gitignore

* Add Nier and Estarriola uncaps

* Fix uncaps treated as new characters

* Redesigned team navigation (#310)

* Add ellipsis icon

* Reduce size of tokens

* Move UpdateToast to toasts folder

* Update variables.scss

* Add reps for grid objects

These reps act like the existing PartyRep except for Characters and Summons, as well as a new component just for Weapons.

They only render the grid of objects and nothing else.

Eventually PartyRep will use WeaponRep

* Added RepSegment

This is a Character, Weapon or Summon rep wrapped with an input and label for use in a SegmentedControl

* Modify PartySegmentedControl to use RepSegments

This will not work on mobile yet, where it should gracefully degrade to a normal SegmentedControl with only text

* Extract URL copied and Remixed toasts into files

* Extract delete team alert into a file

Also, to support this:
* Added `Destructive` class to Button
* Added `primaryActionClassName` prop to Alert

* Added an alert for when remixing teams

* Began refactoring PartyDetails into several files

* PartyHeader will live at the top, above the new segmented control
* PartyDetails stays below, only showing remixed teams and the description
* PartyDropdown handles the new ... menu

* Remove duplicated code

This is description and remix code that is still in `PartyDetails`

* Small fixes for weapon grid

* Add placeholder image for guidebooks

* Add localizations

* Add Guidebook type and update other types

* Update gitignore

Don't commit guidebook images

* Indicate if a dialog is scrollable

We had broken paging in the infinite scroll component. Turning off "scrolling" at the dialog levels fixes it without adding scrollbars in environments that persistently show them

* Add ExtraContainer

This is the purple container that will contain additional weapons and sephira guidebooks

* Move ExtraWeapons to ExtraWeaponsGrid

And put it in ExtraContainer

* Added GuidebooksGrid and GuidebookUnit

These are the display components for Guidebooks in the WeaponGrid

* Visual adjustments to summon grid

* Add Empty class to weapons when unit is unfilled

* Implement GuidebooksGrid in WeaponGrid

* Remove extra switch

* Remove old dependencies and props

* Implement searching for/adding guidebooks to party

* Update styles

* Fix dependency

* Properly determine when extra container should display

* Change to 1-indexing for guidebooks

* Add support for removing guidebooks

* Display guidebook validation error

* Move read only buttons to PartyHeader

Also broke up tokens and made them easier to render

* Add guidebooks to DetailsObject

* Remove preview when on mobile sizes

* Implement raid combobox (#311)

* Add ellipsis icon

* Reduce size of tokens

* Move UpdateToast to toasts folder

* Update variables.scss

* Add reps for grid objects

These reps act like the existing PartyRep except for Characters and Summons, as well as a new component just for Weapons.

They only render the grid of objects and nothing else.

Eventually PartyRep will use WeaponRep

* Added RepSegment

This is a Character, Weapon or Summon rep wrapped with an input and label for use in a SegmentedControl

* Modify PartySegmentedControl to use RepSegments

This will not work on mobile yet, where it should gracefully degrade to a normal SegmentedControl with only text

* Extract URL copied and Remixed toasts into files

* Extract delete team alert into a file

Also, to support this:
* Added `Destructive` class to Button
* Added `primaryActionClassName` prop to Alert

* Added an alert for when remixing teams

* Began refactoring PartyDetails into several files

* PartyHeader will live at the top, above the new segmented control
* PartyDetails stays below, only showing remixed teams and the description
* PartyDropdown handles the new ... menu

* Remove duplicated code

This is description and remix code that is still in `PartyDetails`

* Small fixes for weapon grid

* Add placeholder image for guidebooks

* Add localizations

* Add Guidebook type and update other types

* Update gitignore

Don't commit guidebook images

* Indicate if a dialog is scrollable

We had broken paging in the infinite scroll component. Turning off "scrolling" at the dialog levels fixes it without adding scrollbars in environments that persistently show them

* Add ExtraContainer

This is the purple container that will contain additional weapons and sephira guidebooks

* Move ExtraWeapons to ExtraWeaponsGrid

And put it in ExtraContainer

* Added GuidebooksGrid and GuidebookUnit

These are the display components for Guidebooks in the WeaponGrid

* Visual adjustments to summon grid

* Add Empty class to weapons when unit is unfilled

* Implement GuidebooksGrid in WeaponGrid

* Remove extra switch

* Remove old dependencies and props

* Implement searching for/adding guidebooks to party

* Update styles

* Fix dependency

* Properly determine when extra container should display

* Change to 1-indexing for guidebooks

* Add support for removing guidebooks

* Display guidebook validation error

* Move read only buttons to PartyHeader

Also broke up tokens and made them easier to render

* Add guidebooks to DetailsObject

* Add raid placeholder string to locale

* Update .gitignore

* Update and reorganize localization files

* Update types

Added RaidGroup and updated Raid, then updated dependent types and objects

* Update dependencies

* Update react and react-dom to at least 18.0.0
* Install cmdk

* Rename Arrow.svg to Chevron.svg

Also added a new Arrow.svg with a stem

* Add api call for raidGroups and update pages

Pages fetch raids and store them in the app state. We needed to update this to pull raid groups instead

* Update SegmentedControl component

* Add className and blended properties
* Segment gets flex-grow

* Update Select component

* data-placeholder style should match only if true
* Adjust corner radius to match cards instead of inputs
* Fix classNames call in SelectItem

* Remove raid prop from Party

* Add Popover component

* Popover is a wrapper of Radix's Popover component that we will use to wrap the combobox.
* Move styles that were in PopoverContent.scss to Popover.scss

* Add Command component

The Command component is a wrapper over CMDK's Command component. Pretty much every object in that library is wrapped here. We will use this for the guts of our combobox.

* Add RaidCombobox and RaidItem components

* RaidCombobox combines Popover and Command to create an experience where users can browse through raids by section, search them and sort them.
* RaidItem is effectively a copy-paste of SelectItem using CommandItem, adding some raid-specific styles and elements

* Updates themes and variables

* Replace RaidDropdown with RaidCombobox

* Add small shadow to Tooltip

* Update side offset for Popover

* Update CharLimitedFieldset class name

* Add clear button to Combobox input

* It only shows up when there is text in the input
* Clicking it clears the text in the input
* It uses CharLimitedFieldset's classes

* ChatGPT helped me refactor RaidCombobox

* Further refactoring of RaidCombobox

* Deploy content update (#309)

* Update the updates page with new items (#306)

* Add Nier and Estarriola uncaps (#308)

* Update the updates page with new items (#306) (#307)

* Update .gitignore

* Add Nier and Estarriola uncaps

* Fix uncaps treated as new characters

* Make combobox keyboard accessible

* Style updates

* Refactor accessibility code

* Add translation for "Selected" text

* Change selects to be poppers for consistency

We can't make the new Raid combobox appear over the input like the macOS behavior, so we change all selects to be normal popper behavior

* Set raid groups on teams page

* Implement in FilterBar

* Fix styles for combobox input

* Remove RaidDropdown component

* Update index.scss

* Remove preview when on mobile sizes

* Fix some mobile styles

* Add farming raid option

* Increase height slightly

* Small refactor

* 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

* Fix type error

* Add quick summons (#313)

* Delete yarn.lock

* Add quick summon endpoint

* Add quick summon to GridSummon type

* Add icons

* Add quick summon to SummonUnit

* Quick summon icon is displayed on hover
* Updates the server when clicked

* Fix spacing on WeaponGrid

* Fixes for reactivity and performance (#314)

* Remove editable styles

* Use snapshot for segment reps

Using snapshots lets that data be reactive.

Also removed extra dependencies and fixed a bug in how SummonRep displayed sub-summons

* Don't display QuickSummon on friends, subaura

* Hotfix refreshing when switching tabs

* Another hotfix for tab switching

* Update awakening (#315)

* Add Awakening type and remove old defs

We remove the flat list of awakening data, as we will be pulling data from the database

* Update types to use new Awakening type

* Update WeaponUnit for Grand weapon awakenings

* Update object modals

We needed to update CharacterModal and WeaponModal to display awakenings from the new data format. However, the component used (`SelectWithInput`) was tied to AX Skills in a way that would take exponentially more time to resolve.

Instead, we forked `SelectWithInput` into `AwakeningSelectWithInput` and did our work there.

`AwakeningSelect` was found to be redundant, so it was removed.

* Update hovercards

* Add order to NO_AWAKENING

* Add ability to remove job skills (#317)

* Add Awakening type and remove old defs

We remove the flat list of awakening data, as we will be pulling data from the database

* Update types to use new Awakening type

* Update WeaponUnit for Grand weapon awakenings

* Update object modals

We needed to update CharacterModal and WeaponModal to display awakenings from the new data format. However, the component used (`SelectWithInput`) was tied to AX Skills in a way that would take exponentially more time to resolve.

Instead, we forked `SelectWithInput` into `AwakeningSelectWithInput` and did our work there.

`AwakeningSelect` was found to be redundant, so it was removed.

* Update hovercards

* Add max-height to Select

* Allow styling of Select modal with className prop

* Add Job class to Job select

* Add localizations for removing job skills

* Add endpoint for removing job skills

* Implement removing job skills

We added a (...) button next to each editable job skill that opens a context menu that will allow the user to remove the job skill. An alert is presented to make sure the user is sure before proceeding.

As part of this change, some minor restyling of JobSkillItem was necessary

* Update README.md

Update with new static asset directories

* Quality pass (#326)

* Move min-width to RaidCombobox, not Popover

This fixes #318

* Use snapshots to make tokens reactive

This fixes #319

* Revert ChatGPT refactor of this method

Oops. This code was nice, but it didn't actually assign `false` to keys to be sent to the server. We will revisit this, but it needs to be fixed right now.

This fixes #325

* Ignore gacha directory

We will probably scrape these images soon.

* Add translation for Auto Summon token

* Add auto summon token to app state

* Set battle settings in state on update

Also renames PartyDetails to PartyFooter and makes description reactive

* Stop 1password icon from appearing in name field

* Use snapshot for reactive Edit party modal

* Fix Edit modal placeholder colors

* Fix bug with RaidCombobox and Farming

Selecting farming then opening the raid combobox *twice* consecutively would put you in no segment, so no raids appeared

Fixes #323

* Fix values staying in Edit team even if not saved

The values a user entered in the Edit team modal would persist even if the user hit cancel to close the modal. They wouldn't save to the server, but very confusing nonetheless. Now fixed.

* Fix unreadable colors in ElementToggle

* Fix button alignment in weapon modal

* Add text to filters button on small screens

The FilterBar showed a left aligned filter icon on mobile for months and it was driving me insane

* Remove extraneous code from Header

Including the party name, since it's at the top now

* Fix Alert at small sizes

* Make copy link toast work again

* Remove stylesheet links

* Fix remix toasts and alerts from both locations

The remix toast and alert was barely hooked up and not showing up when invoked from PartyHeader.

It now shows up whether you remix your own team (from PartyDropdown) or if you remix another person's team (from PartyHeader).
This commit is contained in:
Justin Edmund 2023-06-21 03:44:43 -07:00 committed by GitHub
parent b8ae43ddaf
commit c08204dd1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 390 additions and 369 deletions

1
.gitignore vendored
View file

@ -58,6 +58,7 @@ public/images/mastery*
public/images/updates*
public/images/guidebooks*
public/images/raids*
public/images/gacha*
# Typescript v1 declaration files
typings/

View file

@ -57,15 +57,21 @@ root
├─ chara-main/
├─ chara-grid/
├─ chara-square/
├─ guidebooks/
├─ jobs/
├─ job-icons/
├─ job-portraits/
├─ job-skills/
├─ labels/
├─ mastery/
├─ placeholders/
├─ raids/
├─ summon-main/
├─ summon-grid/
├─ summon-square/
├─ updates/
├─ weapon-main/
├─ weapon-grid/
├─ weapon-keys/
├─ weapon-square/
```

View file

@ -47,32 +47,32 @@
&.fire {
background: var(--fire-bg);
color: var(--fire-text);
color: var(--fire-hover-text);
}
&.water {
background: var(--water-bg);
color: var(--water-text);
color: var(--water-hover-text);
}
&.earth {
background: var(--earth-bg);
color: var(--earth-text);
color: var(--earth-hover-text);
}
&.wind {
background: var(--wind-bg);
color: var(--wind-text);
color: var(--wind-hover-text);
}
&.dark {
background: var(--dark-bg);
color: var(--dark-text);
color: var(--dark-hover-text);
}
&.light {
background: var(--light-bg);
color: var(--light-text);
color: var(--light-hover-text);
}
}
}

View file

@ -102,6 +102,23 @@
}
}
.Filter.Button {
justify-content: center;
.Text {
display: none;
width: auto;
@include breakpoint(tablet) {
display: block;
}
@include breakpoint(phone) {
display: block;
}
}
}
.UserInfo {
align-items: center;
display: flex;

View file

@ -181,6 +181,7 @@ const FilterBar = (props: Props) => {
className={filterButtonClasses}
blended={true}
leftAccessoryIcon={<FilterIcon />}
text={t('filters.name')}
onClick={() => setFilterModalOpen(true)}
/>
</div>

View file

@ -7,7 +7,6 @@ import classNames from 'classnames'
import clonedeep from 'lodash.clonedeep'
import Link from 'next/link'
import api from '~utils/api'
import { accountState, initialAccountState } from '~utils/accountState'
import { appState, initialAppState } from '~utils/appState'
import { getLocalId } from '~utils/localId'
@ -32,11 +31,8 @@ import Tooltip from '~components/common/Tooltip'
import * as Switch from '@radix-ui/react-switch'
import ChevronIcon from '~public/icons/Chevron.svg'
import LinkIcon from '~public/icons/Link.svg'
import MenuIcon from '~public/icons/Menu.svg'
import RemixIcon from '~public/icons/Remix.svg'
import PlusIcon from '~public/icons/Add.svg'
import SaveIcon from '~public/icons/Save.svg'
import './index.scss'
@ -51,7 +47,6 @@ const Header = () => {
const localeData = retrieveLocaleCookies()
// State management
const [copyToastOpen, setCopyToastOpen] = useState(false)
const [remixToastOpen, setRemixToastOpen] = useState(false)
const [loginModalOpen, setLoginModalOpen] = useState(false)
const [signupModalOpen, setSignupModalOpen] = useState(false)
@ -64,7 +59,6 @@ const Header = () => {
const [originalName, setOriginalName] = useState('')
// Snapshots
const { account } = useSnapshot(accountState)
const { party: partySnapshot } = useSnapshot(appState)
// Subscribe to app state to listen for party name and
@ -108,15 +102,6 @@ const Header = () => {
setRightMenuOpen(false)
}
// Methods: Event handlers (Copy toast)
function handleCopyToastOpenChanged(open: boolean) {
setCopyToastOpen(open)
}
function handleCopyToastCloseClicked() {
setCopyToastOpen(false)
}
// Methods: Event handlers (Remix toasts)
function handleRemixToastOpenChanged(open: boolean) {
setRemixToastOpen(open)
@ -142,23 +127,6 @@ const Header = () => {
router.push(router.asPath, undefined, { locale: language })
}
function copyToClipboard() {
const path = router.asPath.split('/')[1]
if (path === 'p') {
const el = document.createElement('input')
el.value = window.location.href
el.id = 'url-input'
document.body.appendChild(el)
el.select()
document.execCommand('copy')
el.remove()
setCopyToastOpen(true)
}
}
function logout() {
// Close menu
closeRightMenu()
@ -188,84 +156,6 @@ const Header = () => {
router.push('/new')
}
function remixTeam() {
setOriginalName(partySnapshot.name ? partySnapshot.name : t('no_title'))
if (partySnapshot.shortcode) {
const body = getLocalId()
api
.remix({ shortcode: partySnapshot.shortcode, body: body })
.then((response) => {
const remix = response.data.party
// Store the edit key in local storage
if (remix.edit_key) {
storeEditKey(remix.id, remix.edit_key)
setEditKey(remix.id, remix.user)
}
router.push(`/p/${remix.shortcode}`)
setRemixToastOpen(true)
})
}
}
function toggleFavorite() {
if (partySnapshot.favorited) unsaveFavorite()
else saveFavorite()
}
function saveFavorite() {
if (partySnapshot.id)
api.saveTeam({ id: partySnapshot.id }).then((response) => {
if (response.status == 201) appState.party.favorited = true
})
else console.error('Failed to save team: No party ID')
}
function unsaveFavorite() {
if (partySnapshot.id)
api.unsaveTeam({ id: partySnapshot.id }).then((response) => {
if (response.status == 200) appState.party.favorited = false
})
else console.error('Failed to unsave team: No party ID')
}
// Rendering: Elements
const pageTitle = () => {
let title = ''
let hasAccessory = false
const path = router.asPath.split('/')[1]
if (path === 'p') {
hasAccessory = true
if (appState.party && appState.party.name) {
title = appState.party.name
} else {
title = t('no_title')
}
} else {
title = ''
}
return title !== '' ? (
<Tooltip content={t('tooltips.copy_url')}>
<Button
blended={true}
rightAccessoryIcon={
path === 'p' && hasAccessory ? (
<LinkIcon className="stroke" />
) : undefined
}
text={title}
onClick={copyToClipboard}
/>
</Tooltip>
) : (
''
)
}
const profileImage = () => {
let image
@ -310,21 +200,6 @@ const Header = () => {
)
}
// Rendering: Toasts
const urlCopyToast = () => {
return (
<Toast
altText={t('toasts.copied')}
open={copyToastOpen}
duration={2400}
type="foreground"
content={t('toasts.copied')}
onOpenChange={handleCopyToastOpenChanged}
onCloseClick={handleCopyToastCloseClicked}
/>
)
}
const remixToast = () => {
return (
<Toast
@ -394,7 +269,6 @@ const Header = () => {
</DropdownMenuContent>
</DropdownMenu>
</div>
{!appState.errorCode ? pageTitle() : ''}
</section>
)
}
@ -564,8 +438,6 @@ const Header = () => {
<nav id="Header">
{left()}
{right()}
{urlCopyToast()}
{remixToast()}
{settingsModal()}
{loginModal()}
{signupModal()}

View file

@ -22,9 +22,14 @@
max-width: 30vw;
padding: $unit * 4;
@include breakpoint(tablet) {
max-width: inherit;
max-width: 60vw;
}
@include breakpoint(phone) {
max-width: inherit;
width: 60vw;
width: 70vw;
}
.description {
@ -41,5 +46,15 @@
display: flex;
align-self: flex-end;
gap: $unit;
@include breakpoint(phone) {
flex-direction: column-reverse;
align-self: center;
width: 100%;
.Button {
width: 100%;
}
}
}
}

View file

@ -0,0 +1,3 @@
.Joined .Input::placeholder {
color: var(--text-tertiary);
}

View file

@ -56,6 +56,7 @@ const CharLimitedFieldset: ForwardRefRenderFunction<HTMLInputElement, Props> = (
<div className={classNames({ Joined: true }, props.className)}>
<input
{...props}
data-1p-ignore
autoComplete="off"
className="Input"
type={props.type}

View file

@ -50,6 +50,6 @@
.Input::placeholder {
/* Chrome, Firefox, Opera, Safari 10.1+ */
color: var(--text-secondary) !important;
color: var(--text-secondary);
opacity: 1; /* Firefox */
}

View file

@ -8,7 +8,6 @@
padding: $unit;
transform-origin: var(--radix-popover-content-transform-origin);
width: var(--radix-popover-trigger-width);
min-width: 440px;
z-index: 5;
@include breakpoint(phone) {

View file

@ -19,7 +19,8 @@
color: var(--full-auto-text);
}
&.AutoGuard.On {
&.AutoGuard.On,
&.AutoSummon.On {
background: var(--auto-guard-bg);
color: var(--auto-guard-text);
}

View file

@ -46,7 +46,7 @@ const RemixTeamAlert = ({
<Trans i18nKey="modals.remix_team.description.viewer">
Remixing a team makes a copy of it in your account so you can make
your own changes.\n\nWould you like to remix{' '}
<strong>{{ name: 'HEY' }}</strong>?
<strong>{{ name: name }}</strong>?
</Trans>
)
}

View file

@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from 'react'
import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio'
import { useTranslation } from 'react-i18next'
import {
@ -22,6 +23,8 @@ import TableField from '~components/common/TableField'
import type { DetailsObject } from 'types'
import type { DialogProps } from '@radix-ui/react-dialog'
import { appState } from '~utils/appState'
import CheckIcon from '~public/icons/Check.svg'
import CrossIcon from '~public/icons/Cross.svg'
import './index.scss'
@ -31,14 +34,16 @@ interface Props extends DialogProps {
updateCallback: (details: DetailsObject) => void
}
const EditPartyModal = ({ party, updateCallback, ...props }: Props) => {
const EditPartyModal = ({ updateCallback, ...props }: Props) => {
// Set up router
const router = useRouter()
const locale = router.locale
// Set up translation
const { t } = useTranslation('common')
// Set up reactive state
const { party } = useSnapshot(appState)
// Refs
const headerRef = React.createRef<HTMLDivElement>()
const footerRef = React.createRef<HTMLDivElement>()
@ -54,6 +59,7 @@ const EditPartyModal = ({ party, updateCallback, ...props }: Props) => {
// States: Data
const [name, setName] = useState('')
const [description, setDescription] = useState('')
const [raid, setRaid] = useState<Raid>()
const [extra, setExtra] = useState(false)
const [chargeAttack, setChargeAttack] = useState(true)
@ -68,24 +74,15 @@ const EditPartyModal = ({ party, updateCallback, ...props }: Props) => {
// 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)
persistFromState()
}, [party])
// Methods: Event handlers (Dialog)
function openChange() {
if (open) {
setOpen(false)
setCurrentSegment(0)
persistFromState()
if (props.onOpenChange) props.onOpenChange(false)
} else {
setOpen(true)
@ -176,6 +173,21 @@ const EditPartyModal = ({ party, updateCallback, ...props }: Props) => {
}
// Methods: Data methods
function persistFromState() {
if (!party) return
setName(party.name ? party.name : '')
setDescription(party.description ? party.description : '')
setRaid(party.raid)
setAutoGuard(party.autoGuard)
setAutoSummon(party.autoSummon)
setFullAuto(party.fullAuto)
setChargeAttack(party.chargeAttack)
setClearTime(party.clearTime)
if (party.turnCount) setTurnCount(party.turnCount)
if (party.buttonCount) setButtonCount(party.buttonCount)
if (party.chainCount) setChainCount(party.chainCount)
}
function updateDetails(event: React.MouseEvent) {
const descriptionValue = descriptionInput.current?.value
const details: DetailsObject = {
@ -272,9 +284,8 @@ const EditPartyModal = ({ party, updateCallback, ...props }: Props) => {
}
onChange={handleTextAreaChanged}
ref={descriptionInput}
>
{party ? party.description : ''}
</textarea>
defaultValue={description}
/>
</div>
)
}

View file

@ -7,7 +7,7 @@ import clonedeep from 'lodash.clonedeep'
import Alert from '~components/common/Alert'
import PartySegmentedControl from '~components/party/PartySegmentedControl'
import PartyDetails from '~components/party/PartyDetails'
import PartyFooter from '~components/party/PartyFooter'
import PartyHeader from '~components/party/PartyHeader'
import WeaponGrid from '~components/weapon/WeaponGrid'
import SummonGrid from '~components/summon/SummonGrid'
@ -145,37 +145,27 @@ const Party = (props: Props) => {
function formatDetailsObject(details: DetailsObject) {
const payload: { [key: string]: any } = {}
const mappings: { [key: string]: string } = {
name: 'name',
description: 'description',
chargeAttack: 'charge_attack',
fullAuto: 'full_auto',
autoGuard: 'auto_guard',
autoSummon: 'auto_summon',
clearTime: 'clear_time',
buttonCount: 'button_count',
chainCount: 'chain_count',
turnCount: 'turn_count',
extra: 'extra',
job: 'job_id',
guidebook1_id: 'guidebook1_id',
guidebook2_id: 'guidebook2_id',
guidebook3_id: 'guidebook3_id',
}
Object.entries(mappings).forEach(([key, value]) => {
if (details[key]) {
payload[value] = details[key]
}
})
if (details.name) payload.name = details.name
if (details.description) payload.description = details.description
if (details.raid) payload.raid_id = details.raid.id
if (details.chargeAttack != undefined)
payload.charge_attack = details.chargeAttack
if (details.fullAuto != undefined) payload.full_auto = details.fullAuto
if (details.autoGuard != undefined) payload.auto_guard = details.autoGuard
if (details.autoSummon != undefined)
payload.auto_summon = details.autoSummon
if (details.clearTime) payload.clear_time = details.clearTime
if (details.buttonCount) payload.button_count = details.buttonCount
if (details.chainCount) payload.chain_count = details.chainCount
if (details.turnCount) payload.turn_count = details.turnCount
if (details.extra != undefined) payload.extra = details.extra
if (details.job) payload.job_id = details.job.id
if (details.guidebook1_id) payload.guidebook1_id = details.guidebook1_id
if (details.guidebook2_id) payload.guidebook2_id = details.guidebook2_id
if (details.guidebook3_id) payload.guidebook3_id = details.guidebook3_id
if (Object.keys(payload).length >= 1) {
return { party: payload }
} else {
return {}
}
if (Object.keys(payload).length >= 1) return { party: payload }
else return {}
}
function cancelAlert() {
@ -275,6 +265,15 @@ const Party = (props: Props) => {
appState.party.jobSkills = team.job_skills
appState.party.accessory = team.accessory
appState.party.chargeAttack = team.charge_attack
appState.party.fullAuto = team.full_auto
appState.party.autoGuard = team.auto_guard
appState.party.autoSummon = team.auto_summon
appState.party.clearTime = team.clear_time
appState.party.buttonCount = team.button_count
appState.party.chainCount = team.chain_count
appState.party.turnCount = team.turn_count
appState.party.id = team.id
appState.party.shortcode = team.shortcode
appState.party.extra = team.extra
@ -455,7 +454,7 @@ const Party = (props: Props) => {
<section id="Party">{currentGrid()}</section>
<PartyDetails
<PartyFooter
party={props.team}
new={props.new || false}
editable={party.editable}

View file

@ -2,8 +2,7 @@
import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router'
import { subscribe, useSnapshot } from 'valtio'
import { Trans, useTranslation } from 'next-i18next'
import Link from 'next/link'
import { useTranslation } from 'next-i18next'
import classNames from 'classnames'
// Dependencies: Common
@ -125,22 +124,27 @@ const PartyDropdown = ({
// Toasts / Copy URL
function handleCopyToastOpenChanged(open: boolean) {
setCopyToastOpen(open)
setCopyToastOpen(!open)
}
function handleCopyToastCloseClicked() {
setCopyToastOpen(false)
}
// Toasts / Remix team
// Toasts: Remix team
function handleRemixToastOpenChanged(open: boolean) {
setRemixToastOpen(open)
setRemixToastOpen(!open)
}
function handleRemixToastCloseClicked() {
setRemixToastOpen(false)
}
function remixCallback() {
setRemixToastOpen(true)
remixTeamCallback()
}
const editableItems = () => {
return (
<>
@ -185,10 +189,23 @@ const PartyDropdown = ({
<RemixTeamAlert
creator={editable}
name={partySnapshot.name ? partySnapshot.name : t('no_title')}
name={partySnapshot.name || t('no_title')}
open={remixAlertOpen}
onOpenChange={handleRemixTeamAlertChange}
remixCallback={remixTeamCallback}
remixCallback={remixCallback}
/>
<RemixedToast
open={remixToastOpen}
partyName={partySnapshot.name || t('no_title')}
onOpenChange={handleRemixToastOpenChanged}
onCloseClick={handleRemixToastCloseClicked}
/>
<UrlCopiedToast
open={copyToastOpen}
onOpenChange={handleCopyToastOpenChanged}
onCloseClick={handleCopyToastCloseClicked}
/>
</>
)

View file

@ -1,4 +1,4 @@
.DetailsWrapper {
.FooterWrapper {
display: flex;
flex-direction: column;
gap: $unit-2x;
@ -16,10 +16,13 @@
}
}
.PartyDetails {
.PartyFooter {
box-sizing: border-box;
display: none;
line-height: 1.4;
white-space: pre-wrap;
margin: 0 auto $unit-2x;
margin-bottom: $unit-12x;
min-height: 10vh;
max-width: $unit * 94;
overflow: hidden;
width: 100%;
@ -27,11 +30,6 @@
@include breakpoint(phone) {
padding: 0 $unit;
}
&.Visible {
// margin-bottom: $unit-12x;
}
&.Editable {
gap: $unit;
@ -174,115 +172,109 @@
}
}
&.ReadOnly {
box-sizing: border-box;
line-height: 1.4;
white-space: pre-wrap;
&.Visible {
display: block;
}
&.Visible {
a:hover {
text-decoration: underline;
}
p {
font-size: $font-regular;
line-height: $font-regular * 1.2;
white-space: pre-line;
}
.Details {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: $unit;
margin-bottom: $unit-2x;
}
.YoutubeWrapper {
background-color: var(--card-bg);
border-radius: $card-corner;
margin: $unit 0;
position: relative;
display: block;
contain: content;
background-position: center center;
background-size: cover;
cursor: pointer;
width: 60%;
height: 60%;
@include breakpoint(tablet) {
width: 100%;
height: 100%;
}
/* gradient */
&::before {
content: '';
display: block;
position: absolute;
top: 0;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);
background-position: top;
background-repeat: repeat-x;
height: 60px;
padding-bottom: 50px;
width: 100%;
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
}
a:hover {
text-decoration: underline;
}
p {
font-size: $font-regular;
line-height: $font-regular * 1.2;
white-space: pre-line;
}
.Details {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: $unit;
margin-bottom: $unit-2x;
}
.YoutubeWrapper {
background-color: var(--card-bg);
border-radius: $card-corner;
margin: $unit 0;
position: relative;
display: block;
contain: content;
background-position: center center;
background-size: cover;
cursor: pointer;
width: 60%;
height: 60%;
@include breakpoint(tablet) {
width: 100%;
height: 100%;
}
/* gradient */
&::before {
content: '';
display: block;
position: absolute;
top: 0;
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);
background-position: top;
background-repeat: repeat-x;
height: 60px;
padding-bottom: 50px;
width: 100%;
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
}
/* responsive iframe with a 16:9 aspect ratio
/* responsive iframe with a 16:9 aspect ratio
thanks https://css-tricks.com/responsive-iframes/
*/
&::after {
content: '';
display: block;
padding-bottom: calc(100% / (16 / 9));
}
&::after {
content: '';
display: block;
padding-bottom: calc(100% / (16 / 9));
}
&:hover > .PlayerButton {
opacity: 1;
}
&:hover > .PlayerButton {
opacity: 1;
}
& > iframe {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
& > iframe {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
/* Play button */
& > .PlayerButton {
background: none;
border: none;
background-image: url('/icons/youtube.svg');
width: 68px;
height: 68px;
opacity: 0.8;
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
}
/* Play button */
& > .PlayerButton {
background: none;
border: none;
background-image: url('/icons/youtube.svg');
width: 68px;
height: 68px;
opacity: 0.8;
transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
}
& > .PlayerButton,
& > .PlayerButton:before {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
}
& > .PlayerButton,
& > .PlayerButton:before {
position: absolute;
top: 50%;
left: 50%;
transform: translate3d(-50%, -50%, 0);
}
/* Post-click styles */
&.lyt-activated {
cursor: unset;
}
&.lyt-activated::before,
&.lyt-activated > .PlayerButton {
opacity: 0;
pointer-events: none;
}
/* Post-click styles */
&.lyt-activated {
cursor: unset;
}
&.lyt-activated::before,
&.lyt-activated > .PlayerButton {
opacity: 0;
pointer-events: none;
}
}
}

View file

@ -1,5 +1,6 @@
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'
@ -27,10 +28,12 @@ interface Props {
updateCallback: (details: DetailsObject) => void
}
const PartyDetails = (props: Props) => {
const PartyFooter = (props: Props) => {
const { t } = useTranslation('common')
const router = useRouter()
const { party } = useSnapshot(appState)
const youtubeUrlRegex =
/(?:https:\/\/www\.youtube\.com\/watch\?v=|https:\/\/youtu\.be\/)([\w-]+)/g
@ -40,16 +43,10 @@ const PartyDetails = (props: Props) => {
const [embeddedDescription, setEmbeddedDescription] =
useState<React.ReactNode>()
const readOnlyClasses = classNames({
PartyDetails: true,
ReadOnly: true,
Visible: !open,
})
useEffect(() => {
// Extract the video IDs from the description
if (appState.party.description) {
const videoIds = extractYoutubeVideoIds(appState.party.description)
if (party.description) {
const videoIds = extractYoutubeVideoIds(party.description)
// Fetch the video titles for each ID
const fetchPromises = videoIds.map(({ id }) => fetchYoutubeData(id))
@ -58,7 +55,7 @@ const PartyDetails = (props: Props) => {
Promise.all(fetchPromises).then((videoTitles) => {
// Replace the video URLs in the description with LiteYoutubeEmbed elements
const newDescription = reactStringReplace(
appState.party.description,
party.description,
youtubeUrlRegex,
(match, i) => (
<LiteYouTubeEmbed
@ -77,7 +74,7 @@ const PartyDetails = (props: Props) => {
} else {
setEmbeddedDescription('')
}
}, [appState.party.description])
}, [party.description])
async function fetchYoutubeData(videoId: string) {
return await youtube
@ -173,14 +170,6 @@ const PartyDetails = (props: Props) => {
})
}
const readOnly = () => {
return (
<section className={readOnlyClasses}>
<Linkify>{embeddedDescription}</Linkify>
</section>
)
}
const remixSection = () => {
return (
<section className="Remixes">
@ -192,10 +181,14 @@ const PartyDetails = (props: Props) => {
return (
<>
<section className="DetailsWrapper">{readOnly()}</section>
<section className="FooterWrapper">
<section className="PartyFooter">
<Linkify>{embeddedDescription}</Linkify>
</section>
</section>
{remixes && remixes.length > 0 ? remixSection() : ''}
</>
)
}
export default PartyDetails
export default PartyFooter

View file

@ -12,6 +12,7 @@ import Token from '~components/common/Token'
import EditPartyModal from '~components/party/EditPartyModal'
import PartyDropdown from '~components/party/PartyDropdown'
import api from '~utils/api'
import { accountState } from '~utils/accountState'
import { appState, initialAppState } from '~utils/appState'
import { formatTimeAgo } from '~utils/timeAgo'
@ -23,7 +24,9 @@ import SaveIcon from '~public/icons/Save.svg'
import type { DetailsObject } from 'types'
import './index.scss'
import api from '~utils/api'
import RemixTeamAlert from '~components/dialogs/RemixTeamAlert'
import RemixedToast from '~components/toasts/RemixedToast'
import { set } from 'local-storage'
// Props
interface Props {
@ -44,12 +47,16 @@ const PartyHeader = (props: Props) => {
const { party: partySnapshot } = useSnapshot(appState)
const [name, setName] = useState('')
// State: Component
const [remixAlertOpen, setRemixAlertOpen] = useState(false)
const [remixToastOpen, setRemixToastOpen] = useState(false)
// State: Data
const [name, setName] = useState('')
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)
@ -78,6 +85,7 @@ const PartyHeader = (props: Props) => {
setName(props.party.name)
setAutoGuard(props.party.auto_guard)
setFullAuto(props.party.full_auto)
setAutoSummon(props.party.auto_summon)
setChargeAttack(props.party.charge_attack)
setClearTime(props.party.clear_time)
if (props.party.turn_count) setTurnCount(props.party.turn_count)
@ -155,6 +163,32 @@ const PartyHeader = (props: Props) => {
)
}
// Actions: Remix team
function remixTeamCallback() {
setRemixToastOpen(true)
props.remixCallback()
}
// Alerts: Remix team
function openRemixTeamAlert() {
setRemixAlertOpen(true)
}
function handleRemixTeamAlertChange(open: boolean) {
setRemixAlertOpen(open)
}
// Toasts: Remix team
function handleRemixToastOpenChanged(open: boolean) {
setRemixToastOpen(!open)
}
function handleRemixToastCloseClicked() {
setRemixToastOpen(false)
}
// Rendering
const userBlock = (username?: string, picture?: string, element?: string) => {
return (
<div className={userClass}>
@ -212,12 +246,12 @@ const PartyHeader = (props: Props) => {
<Token
className={classNames({
ChargeAttack: true,
On: chargeAttack,
Off: !chargeAttack,
On: party.chargeAttack,
Off: !party.chargeAttack,
})}
>
{`${t('party.details.labels.charge_attack')} ${
chargeAttack ? 'On' : 'Off'
party.chargeAttack ? 'On' : 'Off'
}`}
</Token>
)
@ -226,11 +260,13 @@ const PartyHeader = (props: Props) => {
<Token
className={classNames({
FullAuto: true,
On: fullAuto,
Off: !fullAuto,
On: party.fullAuto,
Off: !party.fullAuto,
})}
>
{`${t('party.details.labels.full_auto')} ${fullAuto ? 'On' : 'Off'}`}
{`${t('party.details.labels.full_auto')} ${
party.fullAuto ? 'On' : 'Off'
}`}
</Token>
)
@ -238,37 +274,57 @@ const PartyHeader = (props: Props) => {
<Token
className={classNames({
AutoGuard: true,
On: autoGuard,
Off: !autoGuard,
On: party.autoGuard,
Off: !party.autoGuard,
})}
>
{`${t('party.details.labels.auto_guard')} ${autoGuard ? 'On' : 'Off'}`}
{`${t('party.details.labels.auto_guard')} ${
party.autoGuard ? 'On' : 'Off'
}`}
</Token>
)
const autoSummonToken = (
<Token
className={classNames({
AutoSummon: true,
On: party.autoSummon,
Off: !party.autoSummon,
})}
>
{`${t('party.details.labels.auto_summon')} ${
party.autoSummon ? 'On' : 'Off'
}`}
</Token>
)
const turnCountToken = (
<Token>
{t('party.details.turns.with_count', {
count: turnCount,
count: party.turnCount,
})}
</Token>
)
const buttonChainToken = () => {
if (buttonCount || chainCount) {
if (party.buttonCount || party.chainCount) {
let string = ''
if (buttonCount && buttonCount > 0) {
string += `${buttonCount}b`
if (party.buttonCount && party.buttonCount > 0) {
string += `${party.buttonCount}b`
}
if (!buttonCount && chainCount && chainCount > 0) {
string += `0${t('party.details.suffix.buttons')}${chainCount}${t(
if (!party.buttonCount && party.chainCount && party.chainCount > 0) {
string += `0${t('party.details.suffix.buttons')}${party.chainCount}${t(
'party.details.suffix.chains'
)}`
} else if (buttonCount && chainCount && chainCount > 0) {
string += `${chainCount}${t('party.details.suffix.chains')}`
} else if (buttonCount && !chainCount) {
} else if (
party.buttonCount &&
party.chainCount &&
party.chainCount > 0
) {
string += `${party.chainCount}${t('party.details.suffix.chains')}`
} else if (party.buttonCount && !party.chainCount) {
string += `0${t('party.details.suffix.chains')}`
}
@ -277,8 +333,8 @@ const PartyHeader = (props: Props) => {
}
const clearTimeToken = () => {
const minutes = Math.floor(clearTime / 60)
const seconds = clearTime - minutes * 60
const minutes = Math.floor(party.clearTime / 60)
const seconds = party.clearTime - minutes * 60
let string = ''
if (minutes > 0)
@ -296,8 +352,9 @@ const PartyHeader = (props: Props) => {
{chargeAttackToken}
{fullAutoToken}
{autoGuardToken}
{turnCount ? turnCountToken : ''}
{clearTime > 0 ? clearTimeToken() : ''}
{autoSummonToken}
{party.turnCount ? turnCountToken : ''}
{party.clearTime > 0 ? clearTimeToken() : ''}
{buttonChainToken()}
</section>
)
@ -329,7 +386,7 @@ const PartyHeader = (props: Props) => {
leftAccessoryIcon={<RemixIcon />}
className="Remix"
text={t('buttons.remix')}
onClick={props.remixCallback}
onClick={openRemixTeamAlert}
/>
</Tooltip>
)
@ -341,8 +398,8 @@ const PartyHeader = (props: Props) => {
<div className="PartyInfo">
<div className="Left">
<div className="Header">
<h1 className={name ? '' : 'empty'}>
{name ? name : t('no_title')}
<h1 className={party.name ? '' : 'empty'}>
{party.name ? party.name : t('no_title')}
</h1>
{party.remix && party.sourceParty ? (
<Tooltip content={t('tooltips.source')}>
@ -398,6 +455,21 @@ const PartyHeader = (props: Props) => {
</div>
<section className={classes}>{renderTokens()}</section>
</section>
<RemixTeamAlert
creator={props.editable}
name={partySnapshot.name ? partySnapshot.name : t('no_title')}
open={remixAlertOpen}
onOpenChange={handleRemixTeamAlertChange}
remixCallback={remixTeamCallback}
/>
<RemixedToast
open={remixToastOpen}
partyName={props.party?.name || t('no_title')}
onOpenChange={handleRemixToastOpenChanged}
onCloseClick={handleRemixToastCloseClicked}
/>
</>
)
}

View file

@ -1,5 +1,6 @@
.Combobox.Raid {
box-sizing: border-box;
min-width: 440px;
.Header {
background: var(--dialog-bg);
@ -184,6 +185,10 @@
}
}
.SelectTrigger.Raid .Value.Empty {
color: var(--text-tertiary);
}
.Filters .SelectTrigger.Raid {
& > span {
overflow: hidden;

View file

@ -105,19 +105,25 @@ const RaidCombobox = (props: Props) => {
// Set current raid and section when the component mounts
useEffect(() => {
if (appState.party.raid) {
setCurrentRaid(appState.party.raid)
setCurrentSection(appState.party.raid.group.section)
} else if (props.showAllRaidsOption && !currentRaid) {
setCurrentRaid(allRaidsOption)
}
// if (appState.party.raid) {
// setCurrentRaid(appState.party.raid)
// if (appState.party.raid.group.section > 0) {
// setCurrentSection(appState.party.raid.group.section)
// } else {
// setCurrentSection(1)
// }
// } else if (props.showAllRaidsOption && !currentRaid) {
// setCurrentRaid(allRaidsOption)
// }
}, [])
// Set current raid and section when the current raid changes
useEffect(() => {
if (props.currentRaid) {
setCurrentRaid(props.currentRaid)
setCurrentSection(props.currentRaid.group.section)
if (appState.party.raid && appState.party.raid.group.section > 0)
setCurrentSection(props.currentRaid.group.section)
else setCurrentSection(1)
}
}, [props.currentRaid])
@ -260,7 +266,11 @@ const RaidCombobox = (props: Props) => {
// Toggle the open state of the combobox
function toggleOpen() {
if (open) {
if (currentRaid && currentRaid.slug !== 'all') {
if (
currentRaid &&
currentRaid.slug !== 'all' &&
currentRaid.group.section > 0
) {
setCurrentSection(currentRaid.group.section)
}
setScrolled(false)

View file

@ -1,9 +1,7 @@
import React from 'react'
import React, { useEffect } from 'react'
import Toast from '~components/common/Toast'
import { Trans, useTranslation } from 'next-i18next'
import './index.scss'
interface Props {
partyName: string
open: boolean
@ -19,7 +17,9 @@ const RemixedToast = ({
onCloseClick,
}: Props) => {
const { t } = useTranslation('common')
useEffect(() => {
console.log(partyName)
}, [])
// Methods: Event handlers
function handleOpenChange() {
onOpenChange(open)

View file

@ -1,7 +1,5 @@
import React from 'react'
import Toast from '~components/common/Toast'
import './index.scss'
import { useTranslation } from 'next-i18next'
interface Props {

View file

@ -394,12 +394,14 @@ const WeaponModal = ({
{gridWeapon.object.awakenings ? awakeningSelect() : ''}
</div>
<div className="DialogFooter" ref={footerRef}>
<Button
contained={true}
onClick={updateWeapon}
disabled={!formValid}
text={t('modals.weapon.buttons.confirm')}
/>
<div className="actions">
<Button
contained={true}
onClick={updateWeapon}
disabled={!formValid}
text={t('modals.weapon.buttons.confirm')}
/>
</div>
</div>
</DialogContent>
</Dialog>

View file

@ -93,6 +93,7 @@
"unauthorized": "You don't have permission to perform that action"
},
"filters": {
"name": "Filter",
"labels": {
"element": "Element",
"series": "Series",
@ -383,6 +384,7 @@
"charge_attack": "Charge Attack",
"full_auto": "Full Auto",
"auto_guard": "Auto Guard",
"auto_summon": "Auto Summon",
"turn_count": "Turn count",
"button_chain": "Buttons/Chains",
"clear_time": "Clear time"

View file

@ -93,6 +93,7 @@
"unauthorized": "行ったアクションを実行する権限がありません"
},
"filters": {
"name": "フィルター",
"labels": {
"element": "属性",
"series": "シリーズ",
@ -380,6 +381,7 @@
"charge_attack": "奥義",
"full_auto": "フルオート",
"auto_guard": "オートガード",
"auto_summon": "オート召喚",
"turn_count": "経過ターン",
"button_chain": "ポチチェイン",
"clear_time": "討伐時間"

View file

@ -49,6 +49,7 @@ interface AppState {
element: number
fullAuto: boolean
autoGuard: boolean
autoSummon: boolean
chargeAttack: boolean
clearTime: number
buttonCount?: number
@ -110,6 +111,7 @@ export const initialAppState: AppState = {
raid: undefined,
fullAuto: false,
autoGuard: false,
autoSummon: false,
chargeAttack: true,
clearTime: 0,
buttonCount: undefined,