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:39:25 -07:00 committed by GitHub
parent 73b98db85e
commit 103ef7e1a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 384 additions and 369 deletions

1
.gitignore vendored
View file

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

View file

@ -47,32 +47,32 @@
&.fire { &.fire {
background: var(--fire-bg); background: var(--fire-bg);
color: var(--fire-text); color: var(--fire-hover-text);
} }
&.water { &.water {
background: var(--water-bg); background: var(--water-bg);
color: var(--water-text); color: var(--water-hover-text);
} }
&.earth { &.earth {
background: var(--earth-bg); background: var(--earth-bg);
color: var(--earth-text); color: var(--earth-hover-text);
} }
&.wind { &.wind {
background: var(--wind-bg); background: var(--wind-bg);
color: var(--wind-text); color: var(--wind-hover-text);
} }
&.dark { &.dark {
background: var(--dark-bg); background: var(--dark-bg);
color: var(--dark-text); color: var(--dark-hover-text);
} }
&.light { &.light {
background: var(--light-bg); 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 { .UserInfo {
align-items: center; align-items: center;
display: flex; display: flex;

View file

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

View file

@ -7,7 +7,6 @@ import classNames from 'classnames'
import clonedeep from 'lodash.clonedeep' import clonedeep from 'lodash.clonedeep'
import Link from 'next/link' import Link from 'next/link'
import api from '~utils/api'
import { accountState, initialAccountState } from '~utils/accountState' import { accountState, initialAccountState } from '~utils/accountState'
import { appState, initialAppState } from '~utils/appState' import { appState, initialAppState } from '~utils/appState'
import { getLocalId } from '~utils/localId' import { getLocalId } from '~utils/localId'
@ -32,11 +31,8 @@ import Tooltip from '~components/common/Tooltip'
import * as Switch from '@radix-ui/react-switch' import * as Switch from '@radix-ui/react-switch'
import ChevronIcon from '~public/icons/Chevron.svg' import ChevronIcon from '~public/icons/Chevron.svg'
import LinkIcon from '~public/icons/Link.svg'
import MenuIcon from '~public/icons/Menu.svg' import MenuIcon from '~public/icons/Menu.svg'
import RemixIcon from '~public/icons/Remix.svg'
import PlusIcon from '~public/icons/Add.svg' import PlusIcon from '~public/icons/Add.svg'
import SaveIcon from '~public/icons/Save.svg'
import './index.scss' import './index.scss'
@ -51,7 +47,6 @@ const Header = () => {
const localeData = retrieveLocaleCookies() const localeData = retrieveLocaleCookies()
// State management // State management
const [copyToastOpen, setCopyToastOpen] = useState(false)
const [remixToastOpen, setRemixToastOpen] = useState(false) const [remixToastOpen, setRemixToastOpen] = useState(false)
const [loginModalOpen, setLoginModalOpen] = useState(false) const [loginModalOpen, setLoginModalOpen] = useState(false)
const [signupModalOpen, setSignupModalOpen] = useState(false) const [signupModalOpen, setSignupModalOpen] = useState(false)
@ -64,7 +59,6 @@ const Header = () => {
const [originalName, setOriginalName] = useState('') const [originalName, setOriginalName] = useState('')
// Snapshots // Snapshots
const { account } = useSnapshot(accountState)
const { party: partySnapshot } = useSnapshot(appState) const { party: partySnapshot } = useSnapshot(appState)
// Subscribe to app state to listen for party name and // Subscribe to app state to listen for party name and
@ -108,15 +102,6 @@ const Header = () => {
setRightMenuOpen(false) setRightMenuOpen(false)
} }
// Methods: Event handlers (Copy toast)
function handleCopyToastOpenChanged(open: boolean) {
setCopyToastOpen(open)
}
function handleCopyToastCloseClicked() {
setCopyToastOpen(false)
}
// Methods: Event handlers (Remix toasts) // Methods: Event handlers (Remix toasts)
function handleRemixToastOpenChanged(open: boolean) { function handleRemixToastOpenChanged(open: boolean) {
setRemixToastOpen(open) setRemixToastOpen(open)
@ -142,23 +127,6 @@ const Header = () => {
router.push(router.asPath, undefined, { locale: language }) 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() { function logout() {
// Close menu // Close menu
closeRightMenu() closeRightMenu()
@ -188,84 +156,6 @@ const Header = () => {
router.push('/new') 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 = () => { const profileImage = () => {
let image 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 = () => { const remixToast = () => {
return ( return (
<Toast <Toast
@ -394,7 +269,6 @@ const Header = () => {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
</div> </div>
{!appState.errorCode ? pageTitle() : ''}
</section> </section>
) )
} }
@ -564,8 +438,6 @@ const Header = () => {
<nav id="Header"> <nav id="Header">
{left()} {left()}
{right()} {right()}
{urlCopyToast()}
{remixToast()}
{settingsModal()} {settingsModal()}
{loginModal()} {loginModal()}
{signupModal()} {signupModal()}

View file

@ -22,9 +22,14 @@
max-width: 30vw; max-width: 30vw;
padding: $unit * 4; padding: $unit * 4;
@include breakpoint(tablet) {
max-width: inherit;
max-width: 60vw;
}
@include breakpoint(phone) { @include breakpoint(phone) {
max-width: inherit; max-width: inherit;
width: 60vw; width: 70vw;
} }
.description { .description {
@ -41,5 +46,15 @@
display: flex; display: flex;
align-self: flex-end; align-self: flex-end;
gap: $unit; 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)}> <div className={classNames({ Joined: true }, props.className)}>
<input <input
{...props} {...props}
data-1p-ignore
autoComplete="off" autoComplete="off"
className="Input" className="Input"
type={props.type} type={props.type}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import clonedeep from 'lodash.clonedeep'
import Alert from '~components/common/Alert' import Alert from '~components/common/Alert'
import PartySegmentedControl from '~components/party/PartySegmentedControl' 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 PartyHeader from '~components/party/PartyHeader'
import WeaponGrid from '~components/weapon/WeaponGrid' import WeaponGrid from '~components/weapon/WeaponGrid'
import SummonGrid from '~components/summon/SummonGrid' import SummonGrid from '~components/summon/SummonGrid'
@ -145,37 +145,27 @@ const Party = (props: Props) => {
function formatDetailsObject(details: DetailsObject) { function formatDetailsObject(details: DetailsObject) {
const payload: { [key: string]: any } = {} const payload: { [key: string]: any } = {}
const mappings: { [key: string]: string } = { if (details.name) payload.name = details.name
name: 'name', if (details.description) payload.description = details.description
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.raid) payload.raid_id = details.raid.id 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) { if (Object.keys(payload).length >= 1) return { party: payload }
return { party: payload } else return {}
} else {
return {}
}
} }
function cancelAlert() { function cancelAlert() {
@ -275,6 +265,15 @@ const Party = (props: Props) => {
appState.party.jobSkills = team.job_skills appState.party.jobSkills = team.job_skills
appState.party.accessory = team.accessory 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.id = team.id
appState.party.shortcode = team.shortcode appState.party.shortcode = team.shortcode
appState.party.extra = team.extra appState.party.extra = team.extra
@ -455,7 +454,7 @@ const Party = (props: Props) => {
<section id="Party">{currentGrid()}</section> <section id="Party">{currentGrid()}</section>
<PartyDetails <PartyFooter
party={props.team} party={props.team}
new={props.new || false} new={props.new || false}
editable={party.editable} editable={party.editable}

View file

@ -2,8 +2,7 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { subscribe, useSnapshot } from 'valtio' import { subscribe, useSnapshot } from 'valtio'
import { Trans, useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import Link from 'next/link'
import classNames from 'classnames' import classNames from 'classnames'
// Dependencies: Common // Dependencies: Common
@ -125,22 +124,27 @@ const PartyDropdown = ({
// Toasts / Copy URL // Toasts / Copy URL
function handleCopyToastOpenChanged(open: boolean) { function handleCopyToastOpenChanged(open: boolean) {
setCopyToastOpen(open) setCopyToastOpen(!open)
} }
function handleCopyToastCloseClicked() { function handleCopyToastCloseClicked() {
setCopyToastOpen(false) setCopyToastOpen(false)
} }
// Toasts / Remix team // Toasts: Remix team
function handleRemixToastOpenChanged(open: boolean) { function handleRemixToastOpenChanged(open: boolean) {
setRemixToastOpen(open) setRemixToastOpen(!open)
} }
function handleRemixToastCloseClicked() { function handleRemixToastCloseClicked() {
setRemixToastOpen(false) setRemixToastOpen(false)
} }
function remixCallback() {
setRemixToastOpen(true)
remixTeamCallback()
}
const editableItems = () => { const editableItems = () => {
return ( return (
<> <>
@ -185,10 +189,23 @@ const PartyDropdown = ({
<RemixTeamAlert <RemixTeamAlert
creator={editable} creator={editable}
name={partySnapshot.name ? partySnapshot.name : t('no_title')} name={partySnapshot.name || t('no_title')}
open={remixAlertOpen} open={remixAlertOpen}
onOpenChange={handleRemixTeamAlertChange} 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; display: flex;
flex-direction: column; flex-direction: column;
gap: $unit-2x; gap: $unit-2x;
@ -16,10 +16,13 @@
} }
} }
.PartyDetails { .PartyFooter {
box-sizing: border-box; box-sizing: border-box;
display: none; line-height: 1.4;
white-space: pre-wrap;
margin: 0 auto $unit-2x; margin: 0 auto $unit-2x;
margin-bottom: $unit-12x;
min-height: 10vh;
max-width: $unit * 94; max-width: $unit * 94;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
@ -27,11 +30,6 @@
@include breakpoint(phone) { @include breakpoint(phone) {
padding: 0 $unit; padding: 0 $unit;
} }
&.Visible {
// margin-bottom: $unit-12x;
}
&.Editable { &.Editable {
gap: $unit; gap: $unit;
@ -174,11 +172,6 @@
} }
} }
&.ReadOnly {
box-sizing: border-box;
line-height: 1.4;
white-space: pre-wrap;
&.Visible { &.Visible {
display: block; display: block;
} }
@ -285,7 +278,6 @@
} }
} }
} }
}
.PartyInfo { .PartyInfo {
box-sizing: border-box; box-sizing: border-box;

View file

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import clonedeep from 'lodash.clonedeep' import clonedeep from 'lodash.clonedeep'
@ -27,10 +28,12 @@ interface Props {
updateCallback: (details: DetailsObject) => void updateCallback: (details: DetailsObject) => void
} }
const PartyDetails = (props: Props) => { const PartyFooter = (props: Props) => {
const { t } = useTranslation('common') const { t } = useTranslation('common')
const router = useRouter() const router = useRouter()
const { party } = useSnapshot(appState)
const youtubeUrlRegex = const youtubeUrlRegex =
/(?:https:\/\/www\.youtube\.com\/watch\?v=|https:\/\/youtu\.be\/)([\w-]+)/g /(?:https:\/\/www\.youtube\.com\/watch\?v=|https:\/\/youtu\.be\/)([\w-]+)/g
@ -40,16 +43,10 @@ const PartyDetails = (props: Props) => {
const [embeddedDescription, setEmbeddedDescription] = const [embeddedDescription, setEmbeddedDescription] =
useState<React.ReactNode>() useState<React.ReactNode>()
const readOnlyClasses = classNames({
PartyDetails: true,
ReadOnly: true,
Visible: !open,
})
useEffect(() => { useEffect(() => {
// Extract the video IDs from the description // Extract the video IDs from the description
if (appState.party.description) { if (party.description) {
const videoIds = extractYoutubeVideoIds(appState.party.description) const videoIds = extractYoutubeVideoIds(party.description)
// Fetch the video titles for each ID // Fetch the video titles for each ID
const fetchPromises = videoIds.map(({ id }) => fetchYoutubeData(id)) const fetchPromises = videoIds.map(({ id }) => fetchYoutubeData(id))
@ -58,7 +55,7 @@ const PartyDetails = (props: Props) => {
Promise.all(fetchPromises).then((videoTitles) => { Promise.all(fetchPromises).then((videoTitles) => {
// Replace the video URLs in the description with LiteYoutubeEmbed elements // Replace the video URLs in the description with LiteYoutubeEmbed elements
const newDescription = reactStringReplace( const newDescription = reactStringReplace(
appState.party.description, party.description,
youtubeUrlRegex, youtubeUrlRegex,
(match, i) => ( (match, i) => (
<LiteYouTubeEmbed <LiteYouTubeEmbed
@ -77,7 +74,7 @@ const PartyDetails = (props: Props) => {
} else { } else {
setEmbeddedDescription('') setEmbeddedDescription('')
} }
}, [appState.party.description]) }, [party.description])
async function fetchYoutubeData(videoId: string) { async function fetchYoutubeData(videoId: string) {
return await youtube return await youtube
@ -173,14 +170,6 @@ const PartyDetails = (props: Props) => {
}) })
} }
const readOnly = () => {
return (
<section className={readOnlyClasses}>
<Linkify>{embeddedDescription}</Linkify>
</section>
)
}
const remixSection = () => { const remixSection = () => {
return ( return (
<section className="Remixes"> <section className="Remixes">
@ -192,10 +181,14 @@ const PartyDetails = (props: Props) => {
return ( return (
<> <>
<section className="DetailsWrapper">{readOnly()}</section> <section className="FooterWrapper">
<section className="PartyFooter">
<Linkify>{embeddedDescription}</Linkify>
</section>
</section>
{remixes && remixes.length > 0 ? remixSection() : ''} {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 EditPartyModal from '~components/party/EditPartyModal'
import PartyDropdown from '~components/party/PartyDropdown' import PartyDropdown from '~components/party/PartyDropdown'
import api from '~utils/api'
import { accountState } from '~utils/accountState' import { accountState } from '~utils/accountState'
import { appState, initialAppState } from '~utils/appState' import { appState, initialAppState } from '~utils/appState'
import { formatTimeAgo } from '~utils/timeAgo' import { formatTimeAgo } from '~utils/timeAgo'
@ -23,7 +24,9 @@ import SaveIcon from '~public/icons/Save.svg'
import type { DetailsObject } from 'types' import type { DetailsObject } from 'types'
import './index.scss' 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 // Props
interface Props { interface Props {
@ -44,12 +47,16 @@ const PartyHeader = (props: Props) => {
const { party: partySnapshot } = useSnapshot(appState) 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 [chargeAttack, setChargeAttack] = useState(true)
const [fullAuto, setFullAuto] = useState(false) const [fullAuto, setFullAuto] = useState(false)
const [autoGuard, setAutoGuard] = useState(false) const [autoGuard, setAutoGuard] = useState(false)
const [autoSummon, setAutoSummon] = useState(false)
const [buttonCount, setButtonCount] = useState<number | undefined>(undefined) const [buttonCount, setButtonCount] = useState<number | undefined>(undefined)
const [chainCount, setChainCount] = useState<number | undefined>(undefined) const [chainCount, setChainCount] = useState<number | undefined>(undefined)
const [turnCount, setTurnCount] = useState<number | undefined>(undefined) const [turnCount, setTurnCount] = useState<number | undefined>(undefined)
@ -78,6 +85,7 @@ const PartyHeader = (props: Props) => {
setName(props.party.name) setName(props.party.name)
setAutoGuard(props.party.auto_guard) setAutoGuard(props.party.auto_guard)
setFullAuto(props.party.full_auto) setFullAuto(props.party.full_auto)
setAutoSummon(props.party.auto_summon)
setChargeAttack(props.party.charge_attack) setChargeAttack(props.party.charge_attack)
setClearTime(props.party.clear_time) setClearTime(props.party.clear_time)
if (props.party.turn_count) setTurnCount(props.party.turn_count) 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) => { const userBlock = (username?: string, picture?: string, element?: string) => {
return ( return (
<div className={userClass}> <div className={userClass}>
@ -212,12 +246,12 @@ const PartyHeader = (props: Props) => {
<Token <Token
className={classNames({ className={classNames({
ChargeAttack: true, ChargeAttack: true,
On: chargeAttack, On: party.chargeAttack,
Off: !chargeAttack, Off: !party.chargeAttack,
})} })}
> >
{`${t('party.details.labels.charge_attack')} ${ {`${t('party.details.labels.charge_attack')} ${
chargeAttack ? 'On' : 'Off' party.chargeAttack ? 'On' : 'Off'
}`} }`}
</Token> </Token>
) )
@ -226,11 +260,13 @@ const PartyHeader = (props: Props) => {
<Token <Token
className={classNames({ className={classNames({
FullAuto: true, FullAuto: true,
On: fullAuto, On: party.fullAuto,
Off: !fullAuto, Off: !party.fullAuto,
})} })}
> >
{`${t('party.details.labels.full_auto')} ${fullAuto ? 'On' : 'Off'}`} {`${t('party.details.labels.full_auto')} ${
party.fullAuto ? 'On' : 'Off'
}`}
</Token> </Token>
) )
@ -238,37 +274,57 @@ const PartyHeader = (props: Props) => {
<Token <Token
className={classNames({ className={classNames({
AutoGuard: true, AutoGuard: true,
On: autoGuard, On: party.autoGuard,
Off: !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> </Token>
) )
const turnCountToken = ( const turnCountToken = (
<Token> <Token>
{t('party.details.turns.with_count', { {t('party.details.turns.with_count', {
count: turnCount, count: party.turnCount,
})} })}
</Token> </Token>
) )
const buttonChainToken = () => { const buttonChainToken = () => {
if (buttonCount || chainCount) { if (party.buttonCount || party.chainCount) {
let string = '' let string = ''
if (buttonCount && buttonCount > 0) { if (party.buttonCount && party.buttonCount > 0) {
string += `${buttonCount}b` string += `${party.buttonCount}b`
} }
if (!buttonCount && chainCount && chainCount > 0) { if (!party.buttonCount && party.chainCount && party.chainCount > 0) {
string += `0${t('party.details.suffix.buttons')}${chainCount}${t( string += `0${t('party.details.suffix.buttons')}${party.chainCount}${t(
'party.details.suffix.chains' 'party.details.suffix.chains'
)}` )}`
} else if (buttonCount && chainCount && chainCount > 0) { } else if (
string += `${chainCount}${t('party.details.suffix.chains')}` party.buttonCount &&
} else if (buttonCount && !chainCount) { 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')}` string += `0${t('party.details.suffix.chains')}`
} }
@ -277,8 +333,8 @@ const PartyHeader = (props: Props) => {
} }
const clearTimeToken = () => { const clearTimeToken = () => {
const minutes = Math.floor(clearTime / 60) const minutes = Math.floor(party.clearTime / 60)
const seconds = clearTime - minutes * 60 const seconds = party.clearTime - minutes * 60
let string = '' let string = ''
if (minutes > 0) if (minutes > 0)
@ -296,8 +352,9 @@ const PartyHeader = (props: Props) => {
{chargeAttackToken} {chargeAttackToken}
{fullAutoToken} {fullAutoToken}
{autoGuardToken} {autoGuardToken}
{turnCount ? turnCountToken : ''} {autoSummonToken}
{clearTime > 0 ? clearTimeToken() : ''} {party.turnCount ? turnCountToken : ''}
{party.clearTime > 0 ? clearTimeToken() : ''}
{buttonChainToken()} {buttonChainToken()}
</section> </section>
) )
@ -329,7 +386,7 @@ const PartyHeader = (props: Props) => {
leftAccessoryIcon={<RemixIcon />} leftAccessoryIcon={<RemixIcon />}
className="Remix" className="Remix"
text={t('buttons.remix')} text={t('buttons.remix')}
onClick={props.remixCallback} onClick={openRemixTeamAlert}
/> />
</Tooltip> </Tooltip>
) )
@ -341,8 +398,8 @@ const PartyHeader = (props: Props) => {
<div className="PartyInfo"> <div className="PartyInfo">
<div className="Left"> <div className="Left">
<div className="Header"> <div className="Header">
<h1 className={name ? '' : 'empty'}> <h1 className={party.name ? '' : 'empty'}>
{name ? name : t('no_title')} {party.name ? party.name : t('no_title')}
</h1> </h1>
{party.remix && party.sourceParty ? ( {party.remix && party.sourceParty ? (
<Tooltip content={t('tooltips.source')}> <Tooltip content={t('tooltips.source')}>
@ -398,6 +455,21 @@ const PartyHeader = (props: Props) => {
</div> </div>
<section className={classes}>{renderTokens()}</section> <section className={classes}>{renderTokens()}</section>
</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 { .Combobox.Raid {
box-sizing: border-box; box-sizing: border-box;
min-width: 440px;
.Header { .Header {
background: var(--dialog-bg); background: var(--dialog-bg);
@ -184,6 +185,10 @@
} }
} }
.SelectTrigger.Raid .Value.Empty {
color: var(--text-tertiary);
}
.Filters .SelectTrigger.Raid { .Filters .SelectTrigger.Raid {
& > span { & > span {
overflow: hidden; overflow: hidden;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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