Merge pull request #192 from jedmund/fix-181

Ensure header title updates when party name is updated
This commit is contained in:
Justin Edmund 2023-01-28 23:22:09 -08:00 committed by GitHub
commit 5dd81d377d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 340 additions and 269 deletions

View file

@ -4,10 +4,9 @@
border-radius: 6px; border-radius: 6px;
box-shadow: 0 1px 4px rgb(0 0 0 / 8%); box-shadow: 0 1px 4px rgb(0 0 0 / 8%);
box-sizing: border-box; box-sizing: border-box;
min-width: 30vw; width: 30vw;
max-width: 180px;
margin: 0 $unit-2x; margin: 0 $unit-2x;
// top: $unit-8x; // This shouldn't be hardcoded. How to calculate it?
// Also, add space that doesn't make the menu disappear if you move your mouse slowly
z-index: 15; z-index: 15;
@include breakpoint(phone) { @include breakpoint(phone) {

View file

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { useSnapshot } from 'valtio' import { subscribe, useSnapshot } from 'valtio'
import { subscribeKey } from 'valtio/utils'
import { deleteCookie } from 'cookies-next' import { deleteCookie } from 'cookies-next'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { Trans, useTranslation } from 'next-i18next' import { Trans, useTranslation } from 'next-i18next'
@ -9,7 +10,7 @@ import Link from 'next/link'
import api from '~utils/api' import api from '~utils/api'
import { accountState, initialAccountState } from '~utils/accountState' import { accountState, initialAccountState } from '~utils/accountState'
import { appState } from '~utils/appState' import { appState, initialAppState } from '~utils/appState'
import { import {
DropdownMenu, DropdownMenu,
@ -49,28 +50,25 @@ const Header = () => {
const [settingsModalOpen, setSettingsModalOpen] = useState(false) const [settingsModalOpen, setSettingsModalOpen] = useState(false)
const [leftMenuOpen, setLeftMenuOpen] = useState(false) const [leftMenuOpen, setLeftMenuOpen] = useState(false)
const [rightMenuOpen, setRightMenuOpen] = useState(false) const [rightMenuOpen, setRightMenuOpen] = useState(false)
const [name, setName] = useState('')
const [originalName, setOriginalName] = useState('') const [originalName, setOriginalName] = useState('')
// Snapshots // Snapshots
const { account } = useSnapshot(accountState) const { account } = useSnapshot(accountState)
const { party } = useSnapshot(appState) const { party: partySnapshot } = useSnapshot(appState)
function handleCopyToastOpenChanged(open: boolean) { // Subscribe to app state to listen for party name and
setCopyToastOpen(open) // unsubscribe when component is unmounted
} const unsubscribe = subscribe(appState, () => {
const newName =
appState.party && appState.party.name ? appState.party.name : ''
setName(newName)
})
function handleCopyToastCloseClicked() { useEffect(() => () => unsubscribe(), [])
setCopyToastOpen(false)
}
function handleRemixToastOpenChanged(open: boolean) {
setRemixToastOpen(open)
}
function handleRemixToastCloseClicked() {
setRemixToastOpen(false)
}
// Methods: Event handlers (Buttons)
function handleLeftMenuButtonClicked() { function handleLeftMenuButtonClicked() {
setLeftMenuOpen(!leftMenuOpen) setLeftMenuOpen(!leftMenuOpen)
} }
@ -79,9 +77,11 @@ const Header = () => {
setRightMenuOpen(!rightMenuOpen) setRightMenuOpen(!rightMenuOpen)
} }
// Methods: Event handlers (Menus)
function handleLeftMenuOpenChange(open: boolean) { function handleLeftMenuOpenChange(open: boolean) {
setLeftMenuOpen(open) setLeftMenuOpen(open)
} }
function handleRightMenuOpenChange(open: boolean) { function handleRightMenuOpenChange(open: boolean) {
setRightMenuOpen(open) setRightMenuOpen(open)
} }
@ -94,6 +94,41 @@ 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)
function handleRemixToastOpenChanged(open: boolean) {
setRemixToastOpen(open)
}
function handleRemixToastCloseClicked() {
setRemixToastOpen(false)
}
// Methods: Actions
function handleNewParty(event: React.MouseEvent, path: string) {
event.preventDefault()
// Clean state
const resetState = clonedeep(initialAppState)
Object.keys(resetState).forEach((key) => {
appState[key] = resetState[key]
})
// Push the root URL
router.push(path)
// Close right menu
closeRightMenu()
}
function copyToClipboard() { function copyToClipboard() {
const path = router.asPath.split('/')[1] const path = router.asPath.split('/')[1]
@ -111,16 +146,6 @@ const Header = () => {
} }
} }
function handleNewParty(event: React.MouseEvent, path: string) {
event.preventDefault()
// Push the root URL
router.push(path)
// Close right menu
closeRightMenu()
}
function logout() { function logout() {
// Close menu // Close menu
closeRightMenu() closeRightMenu()
@ -139,38 +164,39 @@ const Header = () => {
return false return false
} }
function toggleFavorite() {
if (party.favorited) unsaveFavorite()
else saveFavorite()
}
function saveFavorite() {
if (party.id)
api.saveTeam({ id: party.id }).then((response) => {
if (response.status == 201) appState.party.favorited = true
})
else console.error('Failed to save team: No party ID')
}
function unsaveFavorite() {
if (party.id)
api.unsaveTeam({ id: party.id }).then((response) => {
if (response.status == 200) appState.party.favorited = false
})
else console.error('Failed to unsave team: No party ID')
}
function remixTeam() { function remixTeam() {
setOriginalName(party.name ? party.name : t('no_title')) setOriginalName(partySnapshot.name ? partySnapshot.name : t('no_title'))
if (party.shortcode) if (partySnapshot.shortcode)
api.remix(party.shortcode).then((response) => { api.remix(partySnapshot.shortcode).then((response) => {
const remix = response.data.party const remix = response.data.party
router.push(`/p/${remix.shortcode}`) router.push(`/p/${remix.shortcode}`)
setRemixToastOpen(true) 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 = () => { const pageTitle = () => {
let title = '' let title = ''
let hasAccessory = false let hasAccessory = false
@ -226,6 +252,41 @@ const Header = () => {
return image return image
} }
// Rendering: Buttons
const saveButton = () => {
return (
<Tooltip content={t('tooltips.save')}>
<Button
leftAccessoryIcon={<SaveIcon />}
className={classNames({
Save: true,
Saved: partySnapshot.favorited,
})}
blended={true}
text={
partySnapshot.favorited ? t('buttons.saved') : t('buttons.save')
}
onClick={toggleFavorite}
/>
</Tooltip>
)
}
const remixButton = () => {
return (
<Tooltip content={t('tooltips.remix')}>
<Button
leftAccessoryIcon={<RemixIcon />}
className="Remix"
blended={true}
text={t('buttons.remix')}
onClick={remixTeam}
/>
</Tooltip>
)
}
// Rendering: Toasts
const urlCopyToast = () => { const urlCopyToast = () => {
return ( return (
<Toast <Toast
@ -258,37 +319,7 @@ const Header = () => {
) )
} }
const saveButton = () => { // Rendering: Modals
return (
<Tooltip content={t('tooltips.save')}>
<Button
leftAccessoryIcon={<SaveIcon />}
className={classNames({
Save: true,
Saved: party.favorited,
})}
blended={true}
text={party.favorited ? t('buttons.saved') : t('buttons.save')}
onClick={toggleFavorite}
/>
</Tooltip>
)
}
const remixButton = () => {
return (
<Tooltip content={t('tooltips.remix')}>
<Button
leftAccessoryIcon={<RemixIcon />}
className="Remix"
blended={true}
text={t('buttons.remix')}
onClick={remixTeam}
/>
</Tooltip>
)
}
const settingsModal = () => { const settingsModal = () => {
const user = accountState.account.user const user = accountState.account.user
@ -317,6 +348,7 @@ const Header = () => {
) )
} }
// Rendering: Compositing
const left = () => { const left = () => {
return ( return (
<section> <section>
@ -348,7 +380,7 @@ const Header = () => {
<section> <section>
{router.route === '/p/[party]' && {router.route === '/p/[party]' &&
account.user && account.user &&
(!party.user || party.user.id !== account.user.id) && (!partySnapshot.user || partySnapshot.user.id !== account.user.id) &&
!appState.errorCode !appState.errorCode
? saveButton() ? saveButton()
: ''} : ''}

View file

@ -37,7 +37,6 @@
&.Visible { &.Visible {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
} }
fieldset { fieldset {
@ -340,6 +339,18 @@
font-size: $font-small; font-size: $font-small;
} }
a:visited:not(.fire):not(.water):not(.wind):not(.earth):not(.dark):not(
.light
) {
color: var(--text-primary);
}
a:hover:not(.fire):not(.water):not(.wind):not(.earth):not(.dark):not(
.light
) {
color: $blue;
}
& > *:not(:last-child):after { & > *:not(:last-child):after {
content: ' · '; content: ' · ';
margin: 0 calc($unit / 2); margin: 0 calc($unit / 2);

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState, ChangeEvent, KeyboardEvent } from 'react' import React, { useEffect, useState, ChangeEvent, KeyboardEvent } from 'react'
import Link from 'next/link' import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio' import { subscribe, useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import clonedeep from 'lodash.clonedeep' import clonedeep from 'lodash.clonedeep'
@ -25,7 +25,7 @@ import Token from '~components/Token'
import api from '~utils/api' import api from '~utils/api'
import { accountState } from '~utils/accountState' import { accountState } from '~utils/accountState'
import { appState } from '~utils/appState' import { appState, initialAppState } from '~utils/appState'
import { formatTimeAgo } from '~utils/timeAgo' import { formatTimeAgo } from '~utils/timeAgo'
import { youtube } from '~utils/youtube' import { youtube } from '~utils/youtube'
@ -124,6 +124,26 @@ const PartyDetails = (props: Props) => {
} }
}, [props.party]) }, [props.party])
// Subscribe to router changes and reset state
// if the new route is a new team
useEffect(() => {
router.events.on('routeChangeStart', (url, { shallow }) => {
if (url === '/new' || url === '/') {
const party = initialAppState.party
setName(party.name ? party.name : '')
setAutoGuard(party.autoGuard)
setFullAuto(party.fullAuto)
setChargeAttack(party.chargeAttack)
setClearTime(party.clearTime)
setRemixes(party.remixes)
setTurnCount(party.turnCount)
setButtonCount(party.buttonCount)
setChainCount(party.chainCount)
}
})
}, [])
useEffect(() => { useEffect(() => {
// Extract the video IDs from the description // Extract the video IDs from the description
if (appState.party.description) { if (appState.party.description) {
@ -475,161 +495,163 @@ const PartyDetails = (props: Props) => {
} }
} }
const editable = ( const editable = () => {
<section className={editableClasses}> return (
<CharLimitedFieldset <section className={editableClasses}>
fieldName="name" <CharLimitedFieldset
placeholder="Name your team" fieldName="name"
value={props.party?.name} placeholder="Name your team"
limit={50} value={props.party?.name}
onChange={handleInputChange} limit={50}
error={errors.name} onChange={handleInputChange}
ref={nameInput} error={errors.name}
/> ref={nameInput}
<RaidDropdown />
showAllRaidsOption={false} <RaidDropdown
currentRaid={props.party?.raid ? props.party?.raid.slug : undefined} showAllRaidsOption={false}
onChange={receiveRaid} currentRaid={props.party?.raid ? props.party?.raid.slug : undefined}
/> onChange={receiveRaid}
<ul className="SwitchToggleGroup DetailToggleGroup"> />
<li className="Ougi ToggleSection"> <ul className="SwitchToggleGroup DetailToggleGroup">
<label htmlFor="ougi"> <li className="Ougi ToggleSection">
<span>{t('party.details.labels.charge_attack')}</span> <label htmlFor="ougi">
<div> <span>{t('party.details.labels.charge_attack')}</span>
<Switch <div>
name="charge_attack" <Switch
onCheckedChange={handleChargeAttackChanged} name="charge_attack"
value={switchValue(chargeAttack)} onCheckedChange={handleChargeAttackChanged}
checked={chargeAttack} value={switchValue(chargeAttack)}
/> checked={chargeAttack}
</div> />
</label> </div>
</li> </label>
<li className="FullAuto ToggleSection"> </li>
<label htmlFor="full_auto"> <li className="FullAuto ToggleSection">
<span>{t('party.details.labels.full_auto')}</span> <label htmlFor="full_auto">
<div> <span>{t('party.details.labels.full_auto')}</span>
<Switch <div>
onCheckedChange={handleFullAutoChanged} <Switch
name="full_auto" onCheckedChange={handleFullAutoChanged}
value={switchValue(fullAuto)} name="full_auto"
checked={fullAuto} value={switchValue(fullAuto)}
/> checked={fullAuto}
</div> />
</label> </div>
</li> </label>
<li className="AutoGuard ToggleSection"> </li>
<label htmlFor="auto_guard"> <li className="AutoGuard ToggleSection">
<span>{t('party.details.labels.auto_guard')}</span> <label htmlFor="auto_guard">
<div> <span>{t('party.details.labels.auto_guard')}</span>
<Switch <div>
onCheckedChange={handleAutoGuardChanged} <Switch
name="auto_guard" onCheckedChange={handleAutoGuardChanged}
value={switchValue(autoGuard)} name="auto_guard"
disabled={!fullAuto} value={switchValue(autoGuard)}
checked={autoGuard} disabled={!fullAuto}
/> checked={autoGuard}
</div> />
</label> </div>
</li> </label>
</ul> </li>
<ul className="InputToggleGroup DetailToggleGroup"> </ul>
<li className="InputSection"> <ul className="InputToggleGroup DetailToggleGroup">
<label htmlFor="auto_guard"> <li className="InputSection">
<span>{t('party.details.labels.button_chain')}</span> <label htmlFor="auto_guard">
<div className="Input Bound"> <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 <Input
name="buttons" name="turn_count"
className="AlignRight Bound"
type="number" type="number"
step="1"
min="1"
max="999"
placeholder="0" placeholder="0"
value={`${buttonCount}`} value={`${turnCount}`}
min="0" onChange={handleTurnCountInput}
max="99"
onChange={handleButtonCountInput}
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
/> />
<span>b</span> </label>
<Input </li>
name="chains" <li className="InputSection">
type="number" <label htmlFor="auto_guard">
placeholder="0" <span>{t('party.details.labels.clear_time')}</span>
min="0" <div>
max="99" <DurationInput
value={`${chainCount}`} name="clear_time"
onChange={handleChainCountInput} className="Bound"
onKeyDown={handleInputKeyDown} placeholder="00:00"
/> value={clearTime}
<span>c</span> onValueChange={(value: number) => handleClearTimeInput(value)}
</div> />
</label> </div>
</li> </label>
<li className="InputSection"> </li>
<label htmlFor="auto_guard"> </ul>
<span>{t('party.details.labels.turn_count')}</span> <TextFieldset
<Input fieldName="name"
name="turn_count" placeholder={
className="AlignRight Bound" 'Write your notes here\n\n\nWatch out for the 50% trigger!\nMake sure to click Fediels 3 first\nGood luck with RNG!'
type="number" }
step="1" value={props.party?.description}
min="1" onChange={handleTextAreaChange}
max="999" error={errors.description}
placeholder="0" ref={descriptionInput}
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>
<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="bottom">
<div className="left"> <div className="left">
{router.pathname !== '/new' ? ( {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 <Button
leftAccessoryIcon={<CrossIcon />} leftAccessoryIcon={<CheckIcon className="Check" />}
className="Blended medium destructive" text={t('buttons.save_info')}
onClick={handleClick} onClick={updateDetails}
text={t('buttons.delete')}
/> />
) : ( </div>
''
)}
</div> </div>
<div className="right"> </section>
<Button text={t('buttons.cancel')} onClick={toggleDetails} /> )
<Button }
leftAccessoryIcon={<CheckIcon className="Check" />}
text={t('buttons.save_info')}
onClick={updateDetails}
/>
</div>
</div>
</section>
)
const clearTimeString = () => { const clearTimeString = () => {
const minutes = Math.floor(clearTime / 60) const minutes = Math.floor(clearTime / 60)
@ -664,38 +686,46 @@ const PartyDetails = (props: Props) => {
} }
} }
const readOnly = ( const readOnly = () => {
<section className={readOnlyClasses}> return (
<section className="Details"> <section className={readOnlyClasses}>
<Token className={classNames({ ChargeAttack: true, On: chargeAttack })}> <section className="Details">
{`${t('party.details.labels.charge_attack')} ${ <Token
chargeAttack ? 'On' : 'Off' className={classNames({ ChargeAttack: true, On: chargeAttack })}
}`} >
</Token> {`${t('party.details.labels.charge_attack')} ${
chargeAttack ? 'On' : 'Off'
<Token className={classNames({ FullAuto: true, On: fullAuto })}> }`}
{`${t('party.details.labels.full_auto')} ${fullAuto ? 'On' : 'Off'}`}
</Token>
<Token className={classNames({ AutoGuard: true, On: autoGuard })}>
{`${t('party.details.labels.auto_guard')} ${fullAuto ? 'On' : 'Off'}`}
</Token>
{turnCount ? (
<Token>
{t('party.details.turns.with_count', {
count: turnCount,
})}
</Token> </Token>
) : (
'' <Token className={classNames({ FullAuto: true, On: fullAuto })}>
)} {`${t('party.details.labels.full_auto')} ${
{clearTime > 0 ? <Token>{clearTimeString()}</Token> : ''} fullAuto ? 'On' : 'Off'
{buttonChainToken()} }`}
</Token>
<Token className={classNames({ AutoGuard: true, On: autoGuard })}>
{`${t('party.details.labels.auto_guard')} ${
fullAuto ? 'On' : 'Off'
}`}
</Token>
{turnCount ? (
<Token>
{t('party.details.turns.with_count', {
count: turnCount,
})}
</Token>
) : (
''
)}
{clearTime > 0 ? <Token>{clearTimeString()}</Token> : ''}
{buttonChainToken()}
</section>
<Linkify>{embeddedDescription}</Linkify>
</section> </section>
<Linkify>{embeddedDescription}</Linkify> )
</section> }
)
const remixSection = () => { const remixSection = () => {
return ( return (
@ -755,8 +785,8 @@ const PartyDetails = (props: Props) => {
'' ''
)} )}
</div> </div>
{readOnly} {readOnly()}
{editable} {editable()}
{deleteAlert()} {deleteAlert()}
</section> </section>

View file

@ -35,8 +35,7 @@ const NewRoute: React.FC<Props> = ({
const router = useRouter() const router = useRouter()
function callback(path: string) { function callback(path: string) {
// This is scuffed, how do we do this natively? router.push(path, undefined, { shallow: true })
window.history.replaceState(null, `Grid Tool`, `${path}`)
} }
useEffect(() => { useEffect(() => {