Merge pull request #175 from jedmund/remix

Implement remixes
This commit is contained in:
Justin Edmund 2023-01-28 03:51:31 -08:00 committed by GitHub
commit f293f92e23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 401 additions and 52 deletions

View file

@ -31,6 +31,15 @@
background: transparent; background: transparent;
} }
&.IconButton.medium {
height: inherit;
padding: $unit-half;
&:hover {
background: none;
}
}
&.Contained { &.Contained {
background: var(--button-contained-bg); background: var(--button-contained-bg);

View file

@ -9,7 +9,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, initialAppState } from '~utils/appState' import { appState } from '~utils/appState'
import capitalizeFirstLetter from '~utils/capitalizeFirstLetter' import capitalizeFirstLetter from '~utils/capitalizeFirstLetter'
import { import {
@ -26,12 +26,14 @@ import AccountModal from '~components/AccountModal'
import Toast from '~components/Toast' import Toast from '~components/Toast'
import Button from '~components/Button' import Button from '~components/Button'
import ArrowIcon from '~public/icons/Arrow.svg'
import LinkIcon from '~public/icons/Link.svg' import LinkIcon from '~public/icons/Link.svg'
import MenuIcon from '~public/icons/Menu.svg' import MenuIcon from '~public/icons/Menu.svg'
import ArrowIcon from '~public/icons/Arrow.svg' import RemixIcon from '~public/icons/Remix.svg'
import SaveIcon from '~public/icons/Save.svg' import SaveIcon from '~public/icons/Save.svg'
import './index.scss' import './index.scss'
import Tooltip from '~components/Tooltip'
const Header = () => { const Header = () => {
// Localization // Localization
@ -83,10 +85,6 @@ const Header = () => {
setRightMenuOpen(false) setRightMenuOpen(false)
} }
function handleSettingsOpenChanged(open: boolean) {
setRightMenuOpen(false)
}
function copyToClipboard() { function copyToClipboard() {
const el = document.createElement('input') const el = document.createElement('input')
el.value = window.location.href el.value = window.location.href
@ -106,15 +104,6 @@ const Header = () => {
// Push the root URL // Push the root URL
router.push(path) router.push(path)
// Clean state
const resetState = clonedeep(initialAppState)
Object.keys(resetState).forEach((key) => {
appState[key] = resetState[key]
})
// Set party to be editable
appState.party.editable = true
// Close right menu // Close right menu
closeRightMenu() closeRightMenu()
} }
@ -158,6 +147,14 @@ const Header = () => {
else console.error('Failed to unsave team: No party ID') else console.error('Failed to unsave team: No party ID')
} }
function remixTeam() {
if (party.shortcode)
api.remix(party.shortcode).then((response) => {
const remix = response.data.party
router.push(`/p/${remix.shortcode}`)
})
}
const pageTitle = () => { const pageTitle = () => {
let title = '' let title = ''
let hasAccessory = false let hasAccessory = false
@ -219,7 +216,7 @@ const Header = () => {
open={copyToastOpen} open={copyToastOpen}
duration={2400} duration={2400}
type="foreground" type="foreground"
content="This party's URL was copied to your clipboard" content={t('toasts.copied')}
onOpenChange={handleCopyToastOpenChanged} onOpenChange={handleCopyToastOpenChanged}
onCloseClick={handleCopyToastCloseClicked} onCloseClick={handleCopyToastCloseClicked}
/> />
@ -228,16 +225,32 @@ const Header = () => {
const saveButton = () => { const saveButton = () => {
return ( return (
<Button <Tooltip content={t('tooltips.save')}>
leftAccessoryIcon={<SaveIcon />} <Button
className={classNames({ leftAccessoryIcon={<SaveIcon />}
Save: true, className={classNames({
Saved: party.favorited, Save: true,
})} Saved: party.favorited,
blended={true} })}
text={party.favorited ? 'Saved' : 'Save'} blended={true}
onClick={toggleFavorite} 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>
) )
} }
@ -303,7 +316,7 @@ const Header = () => {
(!party.user || party.user.id !== account.user.id) (!party.user || party.user.id !== account.user.id)
? saveButton() ? saveButton()
: ''} : ''}
{router.route === '/p/[party]' ? remixButton() : ''}
<DropdownMenu <DropdownMenu
open={rightMenuOpen} open={rightMenuOpen}
onOpenChange={handleRightMenuOpenChange} onOpenChange={handleRightMenuOpenChange}

View file

@ -150,9 +150,13 @@ const Party = (props: Props) => {
appState.party.accessory = team.accessory appState.party.accessory = team.accessory
appState.party.id = team.id appState.party.id = team.id
appState.party.shortcode = team.shortcode
appState.party.extra = team.extra appState.party.extra = team.extra
appState.party.user = team.user appState.party.user = team.user
appState.party.favorited = team.favorited appState.party.favorited = team.favorited
appState.party.remix = team.remix
appState.party.remixes = team.remixes
appState.party.sourceParty = team.source_party
appState.party.created_at = team.created_at appState.party.created_at = team.created_at
appState.party.updated_at = team.updated_at appState.party.updated_at = team.updated_at

View file

@ -3,6 +3,7 @@
flex-direction: column; flex-direction: column;
margin: $unit-4x auto 0 auto; margin: $unit-4x auto 0 auto;
max-width: $grid-width; max-width: $grid-width;
padding-bottom: $unit-12x;
@include breakpoint(phone) { @include breakpoint(phone) {
padding: 0 $unit; padding: 0 $unit;
@ -10,13 +11,13 @@
.PartyDetails { .PartyDetails {
display: none; display: none;
margin: 0 auto; margin: 0 auto $unit-2x;
max-width: $unit * 94; max-width: $unit * 94;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
&.Visible { &.Visible {
margin-bottom: $unit-12x; // margin-bottom: $unit-12x;
} }
&.Editable { &.Editable {
@ -269,15 +270,21 @@
.Left { .Left {
flex-grow: 1; flex-grow: 1;
h1 { .Header {
font-size: $font-xlarge; align-items: center;
font-weight: $normal; display: flex;
text-align: left; gap: $unit;
margin-bottom: $unit; margin-bottom: $unit;
color: var(--text-primary);
&.empty { h1 {
color: var(--text-secondary); font-size: $font-xlarge;
font-weight: $normal;
text-align: left;
color: var(--text-primary);
&.empty {
color: var(--text-secondary);
}
} }
} }
@ -332,4 +339,26 @@
} }
} }
} }
.Remixes {
display: flex;
flex-direction: column;
gap: $unit-2x;
width: 752px;
h3 {
font-size: $font-medium;
font-weight: $medium;
}
.GridRepCollection {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
margin-left: $unit-2x * -1;
margin-right: $unit-2x * -1;
.GridRep {
min-width: 200px;
}
}
}
} }

View file

@ -3,6 +3,7 @@ import Link from 'next/link'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import clonedeep from 'lodash.clonedeep'
import Linkify from 'react-linkify' import Linkify from 'react-linkify'
import LiteYouTubeEmbed from 'react-lite-youtube-embed' import LiteYouTubeEmbed from 'react-lite-youtube-embed'
@ -10,17 +11,19 @@ import classNames from 'classnames'
import reactStringReplace from 'react-string-replace' import reactStringReplace from 'react-string-replace'
import Alert from '~components/Alert' import Alert from '~components/Alert'
import Button from '~components/Button' import Button from '~components/Button'
import CharLimitedFieldset from '~components/CharLimitedFieldset' import CharLimitedFieldset from '~components/CharLimitedFieldset'
import Input from '~components/Input'
import DurationInput from '~components/DurationInput' import DurationInput from '~components/DurationInput'
import GridRepCollection from '~components/GridRepCollection'
import GridRep from '~components/GridRep'
import Input from '~components/Input'
import RaidDropdown from '~components/RaidDropdown'
import Switch from '~components/Switch'
import Tooltip from '~components/Tooltip'
import TextFieldset from '~components/TextFieldset'
import Token from '~components/Token' import Token from '~components/Token'
import RaidDropdown from '~components/RaidDropdown' import api from '~utils/api'
import TextFieldset from '~components/TextFieldset'
import Switch from '~components/Switch'
import { accountState } from '~utils/accountState' import { accountState } from '~utils/accountState'
import { appState } from '~utils/appState' import { appState } from '~utils/appState'
import { formatTimeAgo } from '~utils/timeAgo' import { formatTimeAgo } from '~utils/timeAgo'
@ -29,6 +32,7 @@ import { youtube } from '~utils/youtube'
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 EditIcon from '~public/icons/Edit.svg' import EditIcon from '~public/icons/Edit.svg'
import RemixIcon from '~public/icons/Remix.svg'
import type { DetailsObject } from 'types' import type { DetailsObject } from 'types'
@ -69,6 +73,8 @@ const PartyDetails = (props: Props) => {
const [turnCount, setTurnCount] = useState<number | undefined>(undefined) const [turnCount, setTurnCount] = useState<number | undefined>(undefined)
const [clearTime, setClearTime] = useState(0) const [clearTime, setClearTime] = useState(0)
const [remixes, setRemixes] = useState<Party[]>([])
const [raidSlug, setRaidSlug] = useState('') const [raidSlug, setRaidSlug] = useState('')
const [embeddedDescription, setEmbeddedDescription] = const [embeddedDescription, setEmbeddedDescription] =
useState<React.ReactNode>() useState<React.ReactNode>()
@ -111,6 +117,7 @@ const PartyDetails = (props: Props) => {
setFullAuto(props.party.full_auto) setFullAuto(props.party.full_auto)
setChargeAttack(props.party.charge_attack) setChargeAttack(props.party.charge_attack)
setClearTime(props.party.clear_time) setClearTime(props.party.clear_time)
setRemixes(props.party.remixes)
if (props.party.turn_count) setTurnCount(props.party.turn_count) if (props.party.turn_count) setTurnCount(props.party.turn_count)
if (props.party.button_count) setButtonCount(props.party.button_count) if (props.party.button_count) setButtonCount(props.party.button_count)
if (props.party.chain_count) setChainCount(props.party.chain_count) if (props.party.chain_count) setChainCount(props.party.chain_count)
@ -300,6 +307,49 @@ const PartyDetails = (props: Props) => {
props.deleteCallback() props.deleteCallback()
} }
// Methods: Navigation
function goTo(shortcode?: string) {
if (shortcode) router.push(`/p/${shortcode}`)
}
// Methods: Favorites
function toggleFavorite(teamId: string, favorited: boolean) {
if (favorited) unsaveFavorite(teamId)
else saveFavorite(teamId)
}
function saveFavorite(teamId: string) {
api.saveTeam({ id: teamId }).then((response) => {
if (response.status == 201) {
const index = remixes.findIndex((p) => p.id === teamId)
const party = remixes[index]
party.favorited = true
let clonedParties = clonedeep(remixes)
clonedParties[index] = party
setRemixes(clonedParties)
}
})
}
function unsaveFavorite(teamId: string) {
api.unsaveTeam({ id: teamId }).then((response) => {
if (response.status == 200) {
const index = remixes.findIndex((p) => p.id === teamId)
const party = remixes[index]
party.favorited = false
let clonedParties = clonedeep(remixes)
clonedParties[index] = party
setRemixes(clonedParties)
}
})
}
function extractYoutubeVideoIds(text: string) { function extractYoutubeVideoIds(text: string) {
// Initialize an array to store the video IDs // Initialize an array to store the video IDs
const videoIds = [] const videoIds = []
@ -388,6 +438,28 @@ const PartyDetails = (props: Props) => {
) )
} }
function renderRemixes() {
return remixes.map((party, i) => {
return (
<GridRep
id={party.id}
shortcode={party.shortcode}
name={party.name}
createdAt={new Date(party.created_at)}
raid={party.raid}
grid={party.weapons}
user={party.user}
favorited={party.favorited}
fullAuto={party.full_auto}
key={`party-${i}`}
displayUser={true}
onClick={goTo}
onSave={toggleFavorite}
/>
)
})
}
const deleteAlert = () => { const deleteAlert = () => {
if (party.editable) { if (party.editable) {
return ( return (
@ -620,11 +692,35 @@ const PartyDetails = (props: Props) => {
</section> </section>
) )
const remixSection = () => {
return (
<section className="Remixes">
<h3>{t('remixes')}</h3>
{<GridRepCollection>{renderRemixes()}</GridRepCollection>}
</section>
)
}
return ( return (
<section className="DetailsWrapper"> <section className="DetailsWrapper">
<div className="PartyInfo"> <div className="PartyInfo">
<div className="Left"> <div className="Left">
<h1 className={name ? '' : 'empty'}>{name ? name : t('no_title')}</h1> <div className="Header">
<h1 className={name ? '' : 'empty'}>
{name ? name : t('no_title')}
</h1>
{party.remix && party.sourceParty ? (
<Tooltip content={t('tooltips.source')}>
<Button
className="IconButton Blended"
leftAccessoryIcon={<RemixIcon />}
onClick={() => goTo(party.sourceParty?.shortcode)}
/>
</Tooltip>
) : (
''
)}
</div>
<div className="attribution"> <div className="attribution">
{renderUserBlock()} {renderUserBlock()}
{party.raid ? linkedRaidBlock(party.raid) : ''} {party.raid ? linkedRaidBlock(party.raid) : ''}
@ -654,6 +750,7 @@ const PartyDetails = (props: Props) => {
</div> </div>
{readOnly} {readOnly}
{editable} {editable}
{remixes && remixes.length > 0 ? remixSection() : ''}
{deleteAlert()} {deleteAlert()}
</section> </section>
) )

View file

@ -0,0 +1,8 @@
.Tooltip {
background: var(--dialog-bg);
border-radius: $card-corner;
line-height: 1.3;
padding: $unit * 1.5;
z-index: 35;
max-width: 200px;
}

View file

@ -0,0 +1,39 @@
import React, { PropsWithChildren } from 'react'
import classNames from 'classnames'
import * as TooltipPrimitive from '@radix-ui/react-tooltip'
import './index.scss'
interface Props extends TooltipPrimitive.TooltipContentProps {
content: React.ReactNode
open?: boolean
onOpenChange?: (open: boolean) => void
}
export default function Tooltip({
children,
content,
open,
onOpenChange,
...props
}: PropsWithChildren<Props>) {
const classes = classNames(props.className, {
Tooltip: true,
})
return (
<TooltipPrimitive.Root open={open} onOpenChange={onOpenChange}>
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
<TooltipPrimitive.Content
side="top"
align="center"
className={classes}
sideOffset={4}
{...props}
>
{content}
{/* <TooltipPrimitive.Arrow width={11} height={5} /> */}
</TooltipPrimitive.Content>
</TooltipPrimitive.Root>
)
}

87
package-lock.json generated
View file

@ -16,6 +16,7 @@
"@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-switch": "^1.0.1",
"@radix-ui/react-toast": "^1.1.2", "@radix-ui/react-toast": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.0.1", "@radix-ui/react-toggle-group": "^1.0.1",
"@radix-ui/react-tooltip": "^1.0.3",
"@svgr/webpack": "^6.2.0", "@svgr/webpack": "^6.2.0",
"axios": "^0.25.0", "axios": "^0.25.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
@ -2623,6 +2624,52 @@
"react-dom": "^16.8 || ^17.0 || ^18.0" "react-dom": "^16.8 || ^17.0 || ^18.0"
} }
}, },
"node_modules/@radix-ui/react-tooltip": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.3.tgz",
"integrity": "sha512-cmc9qV4KpgqdXVTn1K8KN8MnuSXvw+E719pKwyvpCGrQ+0AA2qTjcIL3uxCj4jc4k3sDR36RF7R3H7N5hPybBQ==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-dismissable-layer": "1.0.2",
"@radix-ui/react-id": "1.0.0",
"@radix-ui/react-popper": "1.1.0",
"@radix-ui/react-portal": "1.0.1",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-slot": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.0",
"@radix-ui/react-visually-hidden": "1.0.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz",
"integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@floating-ui/react-dom": "0.7.2",
"@radix-ui/react-arrow": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-use-callback-ref": "1.0.0",
"@radix-ui/react-use-layout-effect": "1.0.0",
"@radix-ui/react-use-rect": "1.0.0",
"@radix-ui/react-use-size": "1.0.0",
"@radix-ui/rect": "1.0.0"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-use-callback-ref": { "node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz",
@ -9156,6 +9203,46 @@
"@radix-ui/react-use-controllable-state": "1.0.0" "@radix-ui/react-use-controllable-state": "1.0.0"
} }
}, },
"@radix-ui/react-tooltip": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.3.tgz",
"integrity": "sha512-cmc9qV4KpgqdXVTn1K8KN8MnuSXvw+E719pKwyvpCGrQ+0AA2qTjcIL3uxCj4jc4k3sDR36RF7R3H7N5hPybBQ==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-dismissable-layer": "1.0.2",
"@radix-ui/react-id": "1.0.0",
"@radix-ui/react-popper": "1.1.0",
"@radix-ui/react-portal": "1.0.1",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-slot": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.0",
"@radix-ui/react-visually-hidden": "1.0.1"
},
"dependencies": {
"@radix-ui/react-popper": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz",
"integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==",
"requires": {
"@babel/runtime": "^7.13.10",
"@floating-ui/react-dom": "0.7.2",
"@radix-ui/react-arrow": "1.0.1",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-use-callback-ref": "1.0.0",
"@radix-ui/react-use-layout-effect": "1.0.0",
"@radix-ui/react-use-rect": "1.0.0",
"@radix-ui/react-use-size": "1.0.0",
"@radix-ui/rect": "1.0.0"
}
}
}
},
"@radix-ui/react-use-callback-ref": { "@radix-ui/react-use-callback-ref": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz",

View file

@ -21,6 +21,7 @@
"@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-switch": "^1.0.1",
"@radix-ui/react-toast": "^1.1.2", "@radix-ui/react-toast": "^1.1.2",
"@radix-ui/react-toggle-group": "^1.0.1", "@radix-ui/react-toggle-group": "^1.0.1",
"@radix-ui/react-tooltip": "^1.0.3",
"@svgr/webpack": "^6.2.0", "@svgr/webpack": "^6.2.0",
"axios": "^0.25.0", "axios": "^0.25.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",

View file

@ -11,6 +11,7 @@ import setUserToken from '~utils/setUserToken'
import '../styles/globals.scss' import '../styles/globals.scss'
import { ToastProvider, Viewport } from '@radix-ui/react-toast' import { ToastProvider, Viewport } from '@radix-ui/react-toast'
import { TooltipProvider } from '@radix-ui/react-tooltip'
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
const accountCookie = getCookie('account') const accountCookie = getCookie('account')
@ -45,10 +46,12 @@ function MyApp({ Component, pageProps }: AppProps) {
return ( return (
<ThemeProvider> <ThemeProvider>
<ToastProvider swipeDirection="right"> <ToastProvider swipeDirection="right">
<Layout> <TooltipProvider>
<Component {...pageProps} /> <Layout>
</Layout> <Component {...pageProps} />
<Viewport className="ToastViewport" /> </Layout>
<Viewport className="ToastViewport" />
</TooltipProvider>
</ToastProvider> </ToastProvider>
</ThemeProvider> </ThemeProvider>
) )

View file

@ -1,7 +1,9 @@
import React, { useEffect } from 'react' import React, { useEffect } from 'react'
import { useRouter } from 'next/router'
import Head from 'next/head' import Head from 'next/head'
import { useTranslation } from 'next-i18next' import { useTranslation } from 'next-i18next'
import { serverSideTranslations } from 'next-i18next/serverSideTranslations' import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import clonedeep from 'lodash.clonedeep'
import Party from '~components/Party' import Party from '~components/Party'
@ -9,7 +11,7 @@ import api from '~utils/api'
import fetchLatestVersion from '~utils/fetchLatestVersion' import fetchLatestVersion from '~utils/fetchLatestVersion'
import organizeRaids from '~utils/organizeRaids' import organizeRaids from '~utils/organizeRaids'
import setUserToken from '~utils/setUserToken' import setUserToken from '~utils/setUserToken'
import { appState } from '~utils/appState' import { appState, initialAppState } from '~utils/appState'
import { groupWeaponKeys } from '~utils/groupWeaponKeys' import { groupWeaponKeys } from '~utils/groupWeaponKeys'
import { printError } from '~utils/reportError' import { printError } from '~utils/reportError'
@ -29,6 +31,9 @@ const NewRoute: React.FC<Props> = (props: Props) => {
// Import translations // Import translations
const { t } = useTranslation('common') const { t } = useTranslation('common')
// Set up router
const router = useRouter()
function callback(path: string) { function callback(path: string) {
// This is scuffed, how do we do this natively? // This is scuffed, how do we do this natively?
window.history.replaceState(null, `Grid Tool`, `${path}`) window.history.replaceState(null, `Grid Tool`, `${path}`)
@ -38,6 +43,16 @@ const NewRoute: React.FC<Props> = (props: Props) => {
persistStaticData() persistStaticData()
}, [persistStaticData]) }, [persistStaticData])
useEffect(() => {
// Clean state
const resetState = clonedeep(initialAppState)
Object.keys(resetState).forEach((key) => {
appState[key] = resetState[key]
})
// Set party to be editable
appState.party.editable = true
}, [])
function persistStaticData() { function persistStaticData() {
appState.raids = props.raids appState.raids = props.raids
appState.jobs = props.jobs appState.jobs = props.jobs
@ -47,7 +62,7 @@ const NewRoute: React.FC<Props> = (props: Props) => {
} }
return ( return (
<React.Fragment> <React.Fragment key={router.asPath}>
<Head> <Head>
{/* HTML */} {/* HTML */}
<title>{t('page.titles.new')}</title> <title>{t('page.titles.new')}</title>

View file

@ -70,7 +70,7 @@ const PartyRoute: React.FC<Props> = (props: Props) => {
} }
return ( return (
<React.Fragment> <React.Fragment key={router.asPath}>
<Party <Party
team={props.party} team={props.party}
raids={props.sortedRaids} raids={props.sortedRaids}

4
public/icons/Remix.svg Normal file
View file

@ -0,0 +1,4 @@
<svg viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M5.50019 2.00047C5.18025 2.00132 4.83617 2.00371 4.51209 2.01042C3.93815 2.0223 3.42693 2.04775 3.22357 2.10224C2.1883 2.37964 1.37965 3.18828 1.10225 4.22356C1.04775 4.42694 1.02231 4.93829 1.01043 5.51233C1.00003 6.01504 1.00003 6.56583 1.00003 7.00001C1.00003 7.43419 1.00003 7.98498 1.01043 8.48769C1.02231 9.06174 1.04775 9.57308 1.10225 9.77647C1.36232 10.747 2.08929 11.5184 3.03219 11.8396C3.09505 11.861 3.15887 11.8805 3.22357 11.8978C3.42692 11.9523 3.9381 11.9777 4.51201 11.9896C4.7811 11.9952 5.06399 11.9978 5.33539 11.999L4.73226 12.6021C4.537 12.7974 4.537 13.1139 4.73226 13.3092C4.92752 13.5045 5.24411 13.5045 5.43937 13.3092L6.85358 11.895C6.95385 11.7947 7.00263 11.6625 6.99992 11.5311C7.00263 11.3997 6.95385 11.2674 6.85358 11.1672L5.43937 9.75295C5.24411 9.55769 4.92752 9.55769 4.73226 9.75295C4.537 9.94822 4.537 10.2648 4.73226 10.4601L5.27027 10.9981C5.01654 10.9966 4.7638 10.994 4.53092 10.9898C4.01686 10.9805 3.59961 10.9633 3.48239 10.9319C2.83534 10.7585 2.32109 10.2738 2.10695 9.64524C2.09268 9.60333 2.07974 9.56079 2.06818 9.51765C2.03675 9.40038 2.01952 8.98285 2.01025 8.4685C2.00195 8.00794 2.00003 7.46976 2.00003 7.00001C2.00003 6.53027 2.00195 5.99208 2.01025 5.53152C2.01952 5.01718 2.03676 4.59965 2.06818 4.48238C2.25311 3.79219 2.79221 3.2531 3.48239 3.06816C3.59963 3.03675 4.01694 3.01951 4.53106 3.01024C4.83429 3.00477 5.17118 3.00206 5.50006 3.00086C5.7762 2.99984 6.00003 2.77615 6.00003 2.50001C6.00003 2.22386 5.77633 1.99974 5.50019 2.00047Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.7765 11.8999C10.462 11.9842 9.40436 11.9981 8.52156 12.0003L8.4928 12.0004L8.48903 12.0004C8.21325 12.001 7.98986 11.7772 7.98986 11.5015C7.98986 11.2265 8.21208 11.0036 8.48675 11.0013L8.48916 11.0013L8.51572 11.0012C9.39725 10.9977 10.333 10.9834 10.5176 10.934C11.2078 10.749 11.7469 10.2099 11.9319 9.51976C11.9633 9.40248 11.9805 8.98495 11.9898 8.47061C11.9981 8.01005 12 7.47186 12 7.00212C12 6.53237 11.9981 5.99419 11.9898 5.53363C11.9805 5.01928 11.9633 4.60175 11.9319 4.48448C11.9203 4.44134 11.9074 4.3988 11.8931 4.35689C11.6789 3.72829 11.1647 3.24364 10.5176 3.07026C10.4004 3.03886 9.98317 3.02162 9.46911 3.01235C9.23664 3.00815 8.98438 3.00559 8.7311 3.00407L9.26777 3.54074C9.46303 3.736 9.46303 4.05258 9.26777 4.24784C9.0725 4.44311 8.75592 4.44311 8.56066 4.24784L7.14645 2.83363C7.04618 2.73336 6.9974 2.60111 7.00011 2.46972C6.9974 2.33833 7.04618 2.20607 7.14645 2.1058L8.56066 0.69159C8.75592 0.496328 9.0725 0.496328 9.26777 0.69159C9.46303 0.886852 9.46303 1.20343 9.26777 1.3987L8.66331 2.00316C8.93512 2.00436 9.21849 2.00695 9.48802 2.01253C10.0619 2.02441 10.5731 2.04985 10.7765 2.10434C10.8412 2.12168 10.905 2.14109 10.9678 2.1625C11.9107 2.48371 12.6377 3.25509 12.8978 4.22566C12.9523 4.42905 12.9777 4.94039 12.9896 5.51444C13 6.01715 13 6.56794 13 7.00212C13 7.43629 13 7.98709 12.9896 8.48979C12.9777 9.06384 12.9523 9.57519 12.8978 9.77857C12.6204 10.8139 11.8117 11.6225 10.7765 11.8999Z"/>
</svg>

After

Width:  |  Height:  |  Size: 3 KiB

View file

@ -39,6 +39,9 @@
"show_info": "Edit info", "show_info": "Edit info",
"hide_info": "Hide info", "hide_info": "Hide info",
"save_info": "Save info", "save_info": "Save info",
"remix": "Remix",
"save": "Save",
"saved": "Saved",
"menu": "Menu", "menu": "Menu",
"new": "New", "new": "New",
"wiki": "View more on gbf.wiki" "wiki": "View more on gbf.wiki"
@ -384,6 +387,14 @@
"no_skill": "No skill" "no_skill": "No skill"
} }
}, },
"toasts": {
"copied": "This party's URL was copied to your clipboard"
},
"tooltips": {
"remix": "Make a copy of this team",
"save": "Save this team to your account",
"source": "Go to original team"
},
"extra_weapons": "Additional Weapons", "extra_weapons": "Additional Weapons",
"equipped": "Equipped", "equipped": "Equipped",
"coming_soon": "Coming Soon", "coming_soon": "Coming Soon",
@ -394,5 +405,6 @@
"no_user": "Anonymous", "no_user": "Anonymous",
"no_job": "No class", "no_job": "No class",
"no_value": "No value", "no_value": "No value",
"remixes": "Remixes",
"level": "Level" "level": "Level"
} }

View file

@ -39,6 +39,9 @@
"show_info": "詳細を編集", "show_info": "詳細を編集",
"save_info": "詳細を保存", "save_info": "詳細を保存",
"hide_info": "詳細を非表示", "hide_info": "詳細を非表示",
"remix": "リミックス",
"save": "保存する",
"saved": "保存",
"menu": "メニュー", "menu": "メニュー",
"new": "作成", "new": "作成",
"wiki": "gbf.wikiで詳しく見る" "wiki": "gbf.wikiで詳しく見る"
@ -385,6 +388,14 @@
"no_skill": "設定されていません" "no_skill": "設定されていません"
} }
}, },
"toasts": {
"copied": "この編成のURLはクリップボードにコピーされました"
},
"tooltips": {
"remix": "この編成をコピーする",
"save": "この編成をアカウントに保存する",
"source": "オリジナルの編成へ"
},
"equipped": "装備した", "equipped": "装備した",
"extra_weapons": "Additional Weapons", "extra_weapons": "Additional Weapons",
"coming_soon": "開発中", "coming_soon": "開発中",
@ -395,5 +406,6 @@
"no_user": "無名", "no_user": "無名",
"no_job": "ジョブなし", "no_job": "ジョブなし",
"no_value": "値なし", "no_value": "値なし",
"remixes": "リミックスされた編成",
"level": "レベル" "level": "レベル"
} }

View file

@ -153,8 +153,8 @@ $dialog--bg--dark: $grey-25;
// Color Definitions: Menu // Color Definitions: Menu
$menu--bg--light: $grey-100; $menu--bg--light: $grey-100;
$menu--bg--dark: $grey-10; $menu--bg--dark: $grey-10;
$menu--text--light: $grey-90; $menu--text--light: $grey-50;
$menu--text--dark: $grey-50; $menu--text--dark: $grey-60;
$menu--separator--light: $grey-90; $menu--separator--light: $grey-90;
$menu--separator--dark: $grey-05; $menu--separator--dark: $grey-05;
$menu--item--bg--light--hover: $grey-85; $menu--item--bg--light--hover: $grey-85;

3
types/Party.d.ts vendored
View file

@ -18,6 +18,7 @@ interface Party {
button_count?: number button_count?: number
turn_count?: number turn_count?: number
chain_count?: number chain_count?: number
source_party?: Party
job: Job job: Job
job_skills: JobSkillObject job_skills: JobSkillObject
accessory: JobAccessory accessory: JobAccessory
@ -28,6 +29,8 @@ interface Party {
weapons: Array<GridWeapon> weapons: Array<GridWeapon>
summons: Array<GridSummon> summons: Array<GridSummon>
user: User user: User
remix: boolean
remixes: Party[]
created_at: string created_at: string
updated_at: string updated_at: string
} }

View file

@ -120,6 +120,11 @@ class Api {
return axios.get(resourceUrl, params) return axios.get(resourceUrl, params)
} }
remix(shortcode: string, params?: {}) {
const resourceUrl = `${this.url}/parties/${shortcode}/remix`
return axios.post(resourceUrl, params)
}
savedTeams(params: {}) { savedTeams(params: {}) {
const resourceUrl = `${this.url}/parties/favorites` const resourceUrl = `${this.url}/parties/favorites`
return axios.get(resourceUrl, params) return axios.get(resourceUrl, params)

View file

@ -36,6 +36,7 @@ interface AppState {
party: { party: {
id: string | undefined id: string | undefined
shortcode: string | undefined
editable: boolean editable: boolean
detailsVisible: boolean detailsVisible: boolean
name: string | undefined name: string | undefined
@ -55,6 +56,9 @@ interface AppState {
extra: boolean extra: boolean
user: User | undefined user: User | undefined
favorited: boolean favorited: boolean
remix: boolean
remixes: Party[]
sourceParty?: Party
created_at: string created_at: string
updated_at: string updated_at: string
} }
@ -87,6 +91,7 @@ interface AppState {
export const initialAppState: AppState = { export const initialAppState: AppState = {
party: { party: {
id: undefined, id: undefined,
shortcode: '',
editable: false, editable: false,
detailsVisible: false, detailsVisible: false,
name: undefined, name: undefined,
@ -111,6 +116,9 @@ export const initialAppState: AppState = {
extra: false, extra: false,
user: undefined, user: undefined,
favorited: false, favorited: false,
remix: false,
remixes: [],
sourceParty: undefined,
created_at: '', created_at: '',
updated_at: '', updated_at: '',
}, },