Merge branch 'staging' into fix-job-errors
This commit is contained in:
commit
a7e3718a7e
91 changed files with 3661 additions and 1596 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -53,6 +53,7 @@ public/images/chara*
|
|||
public/images/job*
|
||||
public/images/awakening*
|
||||
public/images/ax*
|
||||
public/images/accessory*
|
||||
|
||||
# Typescript v1 declaration files
|
||||
typings/
|
||||
|
|
|
|||
|
|
@ -1,98 +0,0 @@
|
|||
.About.DialogContent {
|
||||
gap: 0;
|
||||
padding-bottom: $unit;
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
padding: 0 $unit-4x;
|
||||
}
|
||||
|
||||
.sections {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: $unit;
|
||||
|
||||
& > h2 {
|
||||
font-size: $font-medium;
|
||||
font-weight: $medium;
|
||||
margin-bottom: $unit * 3;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-regular;
|
||||
line-height: 1.3;
|
||||
margin-bottom: $unit;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.Links {
|
||||
display: grid;
|
||||
gap: $unit;
|
||||
margin: $unit-2x 0;
|
||||
}
|
||||
|
||||
div.LinkItem {
|
||||
margin-top: $unit-2x;
|
||||
}
|
||||
|
||||
.LinkItem {
|
||||
$diameter: $unit-6x;
|
||||
border: 1px solid var(--link-item-bg);
|
||||
border-radius: $card-corner;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--link-item-bg);
|
||||
|
||||
svg {
|
||||
fill: var(--link-item-image-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
padding: $unit-2x;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.Left {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
flex-grow: 1;
|
||||
|
||||
h3 {
|
||||
font-weight: 600;
|
||||
max-width: 70%;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--link-item-image-color);
|
||||
width: $diameter;
|
||||
height: auto;
|
||||
|
||||
&.ShareIcon {
|
||||
width: $unit-4x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,196 +0,0 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '~components/Dialog'
|
||||
import DialogContent from '~components/DialogContent'
|
||||
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import ShareIcon from '~public/icons/Share.svg'
|
||||
import DiscordIcon from '~public/icons/discord.svg'
|
||||
import GithubIcon from '~public/icons/github.svg'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
const AboutModal = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const headerRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t('modals.about.title')}</span>
|
||||
</li>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="About"
|
||||
headerref={headerRef}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onEscapeKeyDown={() => {}}
|
||||
>
|
||||
<div className="DialogHeader" ref={headerRef}>
|
||||
<DialogTitle className="DialogTitle">{t('menu.about')}</DialogTitle>
|
||||
<DialogClose className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</DialogClose>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
<section>
|
||||
<p>
|
||||
Granblue.team is a tool to save and share team comps for{' '}
|
||||
<a
|
||||
href="https://game.granbluefantasy.jp"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Granblue Fantasy
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Start adding to a team and a URL will be created for you to share
|
||||
wherever you like, no account needed.
|
||||
</p>
|
||||
<p>
|
||||
However, if you do make an account, you can save any teams you
|
||||
find for future reference and keep all of your teams together in
|
||||
one place.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Feedback</h2>
|
||||
<p>
|
||||
This is an evolving project so feedback and suggestions are
|
||||
greatly appreciated!
|
||||
</p>
|
||||
<p>
|
||||
If you have a feature request, would like to report a bug, or are
|
||||
enjoying the tool and want to say thanks, come hang out in
|
||||
Discord!
|
||||
</p>
|
||||
<div className="LinkItem">
|
||||
<Link href="https://discord.gg/qyZ5hGdPC8">
|
||||
<a
|
||||
href="https://discord.gg/qyZ5hGdPC8"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="Left">
|
||||
<DiscordIcon />
|
||||
<h3>granblue-tools</h3>
|
||||
</div>
|
||||
<ShareIcon className="ShareIcon" />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Credits</h2>
|
||||
<p>
|
||||
Granblue.team was built by{' '}
|
||||
<a
|
||||
href="https://twitter.com/jedmund"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@jedmund
|
||||
</a>{' '}
|
||||
with a lot of help from{' '}
|
||||
<a
|
||||
href="https://twitter.com/lalalalinna"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@lalalalinna
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a
|
||||
href="https://twitter.com/tarngerine"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@tarngerine
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Many thanks also go to Disinfect, Slipper, Jif, Bless, 9highwind,
|
||||
and everyone else in{' '}
|
||||
<a
|
||||
href="https://game.granbluefantasy.jp/#guild/detail/1190185"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Fireplace
|
||||
</a>{' '}
|
||||
that helped with bug testing and feature requests. (P.S.
|
||||
We're recruiting!) And yoey, but he won't join our crew.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Contributing</h2>
|
||||
<p>
|
||||
This app is open source and licensed under{' '}
|
||||
<a
|
||||
href="https://choosealicense.com/licenses/agpl-3.0/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GNU AGPLv3
|
||||
</a>
|
||||
. Plainly, that means you can download the source, modify it, and
|
||||
redistribute it if you attribute this project, use the same
|
||||
license, and keep it open source. You can contribute on Github.
|
||||
</p>
|
||||
<ul className="Links">
|
||||
<li className="LinkItem">
|
||||
<Link href="https://github.com/jedmund/hensei-api">
|
||||
<a
|
||||
href="https://github.com/jedmund/hensei-api"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="Left">
|
||||
<GithubIcon />
|
||||
<h3>jedmund/hensei-api</h3>
|
||||
</div>
|
||||
<ShareIcon className="ShareIcon" />
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="LinkItem">
|
||||
<Link href="https://github.com/jedmund/hensei-web">
|
||||
<a
|
||||
href="https://github.com/jedmund/hensei-web"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="Left">
|
||||
<GithubIcon />
|
||||
<h3>jedmund/hensei-web</h3>
|
||||
</div>
|
||||
<ShareIcon className="ShareIcon" />
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default AboutModal
|
||||
11
components/AboutPage/index.scss
Normal file
11
components/AboutPage/index.scss
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.About.PageContent {
|
||||
.Links {
|
||||
display: grid;
|
||||
gap: $unit;
|
||||
margin: $unit-2x 0;
|
||||
}
|
||||
|
||||
div.LinkItem {
|
||||
margin-top: $unit-2x;
|
||||
}
|
||||
}
|
||||
165
components/AboutPage/index.tsx
Normal file
165
components/AboutPage/index.tsx
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import ShareIcon from '~public/icons/Share.svg'
|
||||
import DiscordIcon from '~public/icons/discord.svg'
|
||||
import GithubIcon from '~public/icons/github.svg'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const AboutPage: React.FC<Props> = (props: Props) => {
|
||||
const { t: common } = useTranslation('common')
|
||||
return (
|
||||
<div className="About PageContent">
|
||||
<h1>{common('about.segmented_control.about')}</h1>
|
||||
<section>
|
||||
<p>
|
||||
Granblue.team is a tool to save and share team comps for{' '}
|
||||
<a
|
||||
href="https://game.granbluefantasy.jp"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Granblue Fantasy
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Start adding to a team and a URL will be created for you to share
|
||||
wherever you like, no account needed.
|
||||
</p>
|
||||
<p>
|
||||
However, if you do make an account, you can save any teams you find
|
||||
for future reference and keep all of your teams together in one place.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Feedback</h2>
|
||||
<p>
|
||||
This is an evolving project so feedback and suggestions are greatly
|
||||
appreciated!
|
||||
</p>
|
||||
<p>
|
||||
If you have a feature request, would like to report a bug, or are
|
||||
enjoying the tool and want to say thanks, come hang out in Discord!
|
||||
</p>
|
||||
<div className="LinkItem">
|
||||
<Link href="https://discord.gg/qyZ5hGdPC8">
|
||||
<a
|
||||
href="https://discord.gg/qyZ5hGdPC8"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="Left">
|
||||
<DiscordIcon />
|
||||
<h3>granblue-tools</h3>
|
||||
</div>
|
||||
<ShareIcon className="ShareIcon" />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Credits</h2>
|
||||
<p>
|
||||
Granblue.team was built by{' '}
|
||||
<a
|
||||
href="https://twitter.com/jedmund"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@jedmund
|
||||
</a>{' '}
|
||||
with a lot of help from{' '}
|
||||
<a
|
||||
href="https://twitter.com/lalalalinna"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@lalalalinna
|
||||
</a>{' '}
|
||||
and{' '}
|
||||
<a
|
||||
href="https://twitter.com/tarngerine"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
@tarngerine
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Many thanks also go to Disinfect, Slipper, Jif, Bless, 9highwind, and
|
||||
everyone else in{' '}
|
||||
<a
|
||||
href="https://game.granbluefantasy.jp/#guild/detail/1190185"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Fireplace
|
||||
</a>{' '}
|
||||
that helped with bug testing and feature requests. (P.S. We're
|
||||
recruiting!) And yoey, but he won't join our crew.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Contributing</h2>
|
||||
<p>
|
||||
This app is open source and licensed under{' '}
|
||||
<a
|
||||
href="https://choosealicense.com/licenses/agpl-3.0/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
GNU AGPLv3
|
||||
</a>
|
||||
. Plainly, that means you can download the source, modify it, and
|
||||
redistribute it if you attribute this project, use the same license,
|
||||
and keep it open source. You can contribute on Github.
|
||||
</p>
|
||||
<ul className="Links">
|
||||
<li className="LinkItem">
|
||||
<Link href="https://github.com/jedmund/hensei-api">
|
||||
<a
|
||||
href="https://github.com/jedmund/hensei-api"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="Left">
|
||||
<GithubIcon />
|
||||
<h3>jedmund/hensei-api</h3>
|
||||
</div>
|
||||
<ShareIcon className="ShareIcon" />
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="LinkItem">
|
||||
<Link href="https://github.com/jedmund/hensei-web">
|
||||
<a
|
||||
href="https://github.com/jedmund/hensei-web"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="Left">
|
||||
<GithubIcon />
|
||||
<h3>jedmund/hensei-web</h3>
|
||||
</div>
|
||||
<ShareIcon className="ShareIcon" />
|
||||
</a>
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AboutPage
|
||||
|
|
@ -34,298 +34,312 @@ type StateVariables = {
|
|||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
username?: string
|
||||
picture?: string
|
||||
gender?: number
|
||||
language?: string
|
||||
theme?: string
|
||||
private?: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
const AccountModal = (props: Props) => {
|
||||
// Localization
|
||||
const { t } = useTranslation('common')
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
const AccountModal = React.forwardRef<HTMLDivElement, Props>(
|
||||
function AccountModal(props: Props, forwardedRef) {
|
||||
// Localization
|
||||
const { t } = useTranslation('common')
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale)
|
||||
? router.locale
|
||||
: 'en'
|
||||
|
||||
// useEffect only runs on the client, so now we can safely show the UI
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme: appTheme, setTheme: setAppTheme } = useTheme()
|
||||
// useEffect only runs on the client, so now we can safely show the UI
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { theme: appTheme, setTheme: setAppTheme } = useTheme()
|
||||
|
||||
// Cookies
|
||||
const accountCookie = getCookie('account')
|
||||
const userCookie = getCookie('user')
|
||||
// Cookies
|
||||
const accountCookie = getCookie('account')
|
||||
const userCookie = getCookie('user')
|
||||
|
||||
const cookieData = {
|
||||
account: accountCookie ? JSON.parse(accountCookie as string) : undefined,
|
||||
user: userCookie ? JSON.parse(userCookie as string) : undefined,
|
||||
}
|
||||
|
||||
// UI State
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectOpenState, setSelectOpenState] = useState<StateVariables>({
|
||||
picture: false,
|
||||
gender: false,
|
||||
language: false,
|
||||
theme: false,
|
||||
})
|
||||
|
||||
// Values
|
||||
const [username, setUsername] = useState(props.username || '')
|
||||
const [picture, setPicture] = useState(props.picture || '')
|
||||
const [language, setLanguage] = useState(props.language || '')
|
||||
const [gender, setGender] = useState(props.gender || 0)
|
||||
const [theme, setTheme] = useState(props.theme || 'system')
|
||||
// const [privateProfile, setPrivateProfile] = useState(false)
|
||||
|
||||
// Setup
|
||||
const [pictureOpen, setPictureOpen] = useState(false)
|
||||
const [genderOpen, setGenderOpen] = useState(false)
|
||||
const [languageOpen, setLanguageOpen] = useState(false)
|
||||
const [themeOpen, setThemeOpen] = useState(false)
|
||||
|
||||
// Refs
|
||||
const headerRef = React.createRef<HTMLDivElement>()
|
||||
const footerRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
// UI management
|
||||
function openChange(open: boolean) {
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
function openSelect(name: 'picture' | 'gender' | 'language' | 'theme') {
|
||||
setPictureOpen(name === 'picture' ? !pictureOpen : false)
|
||||
setGenderOpen(name === 'gender' ? !genderOpen : false)
|
||||
setLanguageOpen(name === 'language' ? !languageOpen : false)
|
||||
setThemeOpen(name === 'theme' ? !themeOpen : false)
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
function handlePictureChange(value: string) {
|
||||
setPicture(value)
|
||||
}
|
||||
|
||||
function handleLanguageChange(value: string) {
|
||||
setLanguage(value)
|
||||
}
|
||||
|
||||
function handleGenderChange(value: string) {
|
||||
setGender(parseInt(value))
|
||||
}
|
||||
|
||||
function handleThemeChange(value: string) {
|
||||
setTheme(value)
|
||||
setAppTheme(value)
|
||||
}
|
||||
|
||||
function onEscapeKeyDown(event: KeyboardEvent) {
|
||||
if (pictureOpen || genderOpen || languageOpen || themeOpen) {
|
||||
return event.preventDefault()
|
||||
} else {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
// API calls
|
||||
function update(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
const object = {
|
||||
user: {
|
||||
picture: picture,
|
||||
element: pictureData.find((i) => i.filename === picture)?.element,
|
||||
language: language,
|
||||
gender: gender,
|
||||
theme: theme,
|
||||
// private: privateProfile,
|
||||
},
|
||||
const cookieData = {
|
||||
account: accountCookie ? JSON.parse(accountCookie as string) : undefined,
|
||||
user: userCookie ? JSON.parse(userCookie as string) : undefined,
|
||||
}
|
||||
|
||||
if (accountState.account.user) {
|
||||
api.endpoints.users
|
||||
.update(accountState.account.user?.id, object)
|
||||
.then((response) => {
|
||||
const user = response.data
|
||||
|
||||
const cookieObj = {
|
||||
picture: user.avatar.picture,
|
||||
element: user.avatar.element,
|
||||
gender: user.gender,
|
||||
language: user.language,
|
||||
theme: user.theme,
|
||||
}
|
||||
|
||||
setCookie('user', cookieObj, { path: '/' })
|
||||
|
||||
accountState.account.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
picture: user.avatar.picture,
|
||||
element: user.avatar.element,
|
||||
language: user.language,
|
||||
theme: user.theme,
|
||||
gender: user.gender,
|
||||
}
|
||||
|
||||
setOpen(false)
|
||||
changeLanguage(router, user.language)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Views
|
||||
const pictureOptions = pictureData
|
||||
.sort((a, b) => (a.name.en > b.name.en ? 1 : -1))
|
||||
.map((item, i) => {
|
||||
return (
|
||||
<PictureSelectItem
|
||||
key={`picture-${i}`}
|
||||
element={item.element}
|
||||
src={[
|
||||
`/profile/${item.filename}.png`,
|
||||
`/profile/${item.filename}@2x.png 2x`,
|
||||
]}
|
||||
value={item.filename}
|
||||
>
|
||||
{item.name[locale]}
|
||||
</PictureSelectItem>
|
||||
)
|
||||
// UI State
|
||||
const [open, setOpen] = useState(false)
|
||||
const [selectOpenState, setSelectOpenState] = useState<StateVariables>({
|
||||
picture: false,
|
||||
gender: false,
|
||||
language: false,
|
||||
theme: false,
|
||||
})
|
||||
|
||||
const pictureField = () => (
|
||||
<SelectTableField
|
||||
name="picture"
|
||||
description={t('modals.settings.descriptions.picture')}
|
||||
className="Image"
|
||||
label={t('modals.settings.labels.picture')}
|
||||
open={pictureOpen}
|
||||
onOpenChange={() => openSelect('picture')}
|
||||
onChange={handlePictureChange}
|
||||
onClose={() => setPictureOpen(false)}
|
||||
imageAlt={t('modals.settings.labels.image_alt')}
|
||||
imageClass={pictureData.find((i) => i.filename === picture)?.element}
|
||||
imageSrc={[`/profile/${picture}.png`, `/profile/${picture}@2x.png 2x`]}
|
||||
value={picture}
|
||||
>
|
||||
{pictureOptions}
|
||||
</SelectTableField>
|
||||
)
|
||||
// Values
|
||||
const [username, setUsername] = useState(props.username || '')
|
||||
const [picture, setPicture] = useState(props.picture || '')
|
||||
const [language, setLanguage] = useState(props.language || '')
|
||||
const [gender, setGender] = useState(props.gender || 0)
|
||||
const [theme, setTheme] = useState(props.theme || 'system')
|
||||
// const [privateProfile, setPrivateProfile] = useState(false)
|
||||
|
||||
const genderField = () => (
|
||||
<SelectTableField
|
||||
name="gender"
|
||||
description={t('modals.settings.descriptions.gender')}
|
||||
label={t('modals.settings.labels.gender')}
|
||||
open={genderOpen}
|
||||
onOpenChange={() => openSelect('gender')}
|
||||
onChange={handleGenderChange}
|
||||
onClose={() => setGenderOpen(false)}
|
||||
value={`${gender}`}
|
||||
>
|
||||
<SelectItem key="gran" value="0">
|
||||
{t('modals.settings.gender.gran')}
|
||||
</SelectItem>
|
||||
<SelectItem key="djeeta" value="1">
|
||||
{t('modals.settings.gender.djeeta')}
|
||||
</SelectItem>
|
||||
</SelectTableField>
|
||||
)
|
||||
// Setup
|
||||
const [pictureOpen, setPictureOpen] = useState(false)
|
||||
const [genderOpen, setGenderOpen] = useState(false)
|
||||
const [languageOpen, setLanguageOpen] = useState(false)
|
||||
const [themeOpen, setThemeOpen] = useState(false)
|
||||
|
||||
const languageField = () => (
|
||||
<SelectTableField
|
||||
name="language"
|
||||
label={t('modals.settings.labels.language')}
|
||||
open={languageOpen}
|
||||
onOpenChange={() => openSelect('language')}
|
||||
onChange={handleLanguageChange}
|
||||
onClose={() => setLanguageOpen(false)}
|
||||
value={language}
|
||||
>
|
||||
<SelectItem key="en" value="en">
|
||||
{t('modals.settings.language.english')}
|
||||
</SelectItem>
|
||||
<SelectItem key="ja" value="ja">
|
||||
{t('modals.settings.language.japanese')}
|
||||
</SelectItem>
|
||||
</SelectTableField>
|
||||
)
|
||||
// Refs
|
||||
const headerRef = React.createRef<HTMLDivElement>()
|
||||
const footerRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
const themeField = () => (
|
||||
<SelectTableField
|
||||
name="theme"
|
||||
label={t('modals.settings.labels.theme')}
|
||||
open={themeOpen}
|
||||
onOpenChange={() => openSelect('theme')}
|
||||
onChange={handleThemeChange}
|
||||
onClose={() => setThemeOpen(false)}
|
||||
value={theme}
|
||||
>
|
||||
<SelectItem key="system" value="system">
|
||||
{t('modals.settings.theme.system')}
|
||||
</SelectItem>
|
||||
<SelectItem key="light" value="light">
|
||||
{t('modals.settings.theme.light')}
|
||||
</SelectItem>
|
||||
<SelectItem key="dark" value="dark">
|
||||
{t('modals.settings.theme.dark')}
|
||||
</SelectItem>
|
||||
</SelectTableField>
|
||||
)
|
||||
useEffect(() => {
|
||||
setOpen(props.open)
|
||||
}, [props.open])
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
// UI management
|
||||
function openChange(open: boolean) {
|
||||
if (props.onOpenChange) props.onOpenChange(open)
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
function openSelect(name: 'picture' | 'gender' | 'language' | 'theme') {
|
||||
setPictureOpen(name === 'picture' ? !pictureOpen : false)
|
||||
setGenderOpen(name === 'gender' ? !genderOpen : false)
|
||||
setLanguageOpen(name === 'language' ? !languageOpen : false)
|
||||
setThemeOpen(name === 'theme' ? !themeOpen : false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={openChange}>
|
||||
<DialogTrigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t('menu.settings')}</span>
|
||||
</li>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="Account"
|
||||
headerref={headerRef}
|
||||
footerref={footerRef}
|
||||
onOpenAutoFocus={(event: Event) => {}}
|
||||
onEscapeKeyDown={onEscapeKeyDown}
|
||||
// Event handlers
|
||||
function handlePictureChange(value: string) {
|
||||
setPicture(value)
|
||||
}
|
||||
|
||||
function handleLanguageChange(value: string) {
|
||||
setLanguage(value)
|
||||
}
|
||||
|
||||
function handleGenderChange(value: string) {
|
||||
setGender(parseInt(value))
|
||||
}
|
||||
|
||||
function handleThemeChange(value: string) {
|
||||
setTheme(value)
|
||||
setAppTheme(value)
|
||||
}
|
||||
|
||||
function onEscapeKeyDown(event: KeyboardEvent) {
|
||||
if (pictureOpen || genderOpen || languageOpen || themeOpen) {
|
||||
return event.preventDefault()
|
||||
} else {
|
||||
setOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
// API calls
|
||||
function update(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
const object = {
|
||||
user: {
|
||||
picture: picture,
|
||||
element: pictureData.find((i) => i.filename === picture)?.element,
|
||||
language: language,
|
||||
gender: gender,
|
||||
theme: theme,
|
||||
// private: privateProfile,
|
||||
},
|
||||
}
|
||||
|
||||
if (accountState.account.user) {
|
||||
api.endpoints.users
|
||||
.update(accountState.account.user?.id, object)
|
||||
.then((response) => {
|
||||
const user = response.data
|
||||
|
||||
const cookieObj = {
|
||||
avatar: {
|
||||
picture: user.avatar.picture,
|
||||
element: user.avatar.element,
|
||||
},
|
||||
gender: user.gender,
|
||||
language: user.language,
|
||||
theme: user.theme,
|
||||
}
|
||||
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 60)
|
||||
setCookie('user', cookieObj, { path: '/', expires: expiresAt })
|
||||
|
||||
accountState.account.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
granblueId: '',
|
||||
avatar: {
|
||||
picture: user.avatar.picture,
|
||||
element: user.avatar.element,
|
||||
},
|
||||
language: user.language,
|
||||
theme: user.theme,
|
||||
gender: user.gender,
|
||||
}
|
||||
|
||||
setOpen(false)
|
||||
if (props.onOpenChange) props.onOpenChange(false)
|
||||
changeLanguage(router, user.language)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Views
|
||||
const pictureOptions = pictureData
|
||||
.sort((a, b) => (a.name.en > b.name.en ? 1 : -1))
|
||||
.map((item, i) => {
|
||||
return (
|
||||
<PictureSelectItem
|
||||
key={`picture-${i}`}
|
||||
element={item.element}
|
||||
src={[
|
||||
`/profile/${item.filename}.png`,
|
||||
`/profile/${item.filename}@2x.png 2x`,
|
||||
]}
|
||||
value={item.filename}
|
||||
>
|
||||
{item.name[locale]}
|
||||
</PictureSelectItem>
|
||||
)
|
||||
})
|
||||
|
||||
const pictureField = () => (
|
||||
<SelectTableField
|
||||
name="picture"
|
||||
description={t('modals.settings.descriptions.picture')}
|
||||
className="Image"
|
||||
label={t('modals.settings.labels.picture')}
|
||||
open={pictureOpen}
|
||||
onOpenChange={() => openSelect('picture')}
|
||||
onChange={handlePictureChange}
|
||||
onClose={() => setPictureOpen(false)}
|
||||
imageAlt={t('modals.settings.labels.image_alt')}
|
||||
imageClass={pictureData.find((i) => i.filename === picture)?.element}
|
||||
imageSrc={[`/profile/${picture}.png`, `/profile/${picture}@2x.png 2x`]}
|
||||
value={picture}
|
||||
>
|
||||
<div className="DialogHeader" ref={headerRef}>
|
||||
<div className="DialogTop">
|
||||
<DialogTitle className="SubTitle">
|
||||
{t('modals.settings.title')}
|
||||
</DialogTitle>
|
||||
<DialogTitle className="DialogTitle">@{username}</DialogTitle>
|
||||
</div>
|
||||
<DialogClose className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</DialogClose>
|
||||
</div>
|
||||
{pictureOptions}
|
||||
</SelectTableField>
|
||||
)
|
||||
|
||||
<form onSubmit={update}>
|
||||
<div className="Fields">
|
||||
{pictureField()}
|
||||
{genderField()}
|
||||
{languageField()}
|
||||
{themeField()}
|
||||
const genderField = () => (
|
||||
<SelectTableField
|
||||
name="gender"
|
||||
description={t('modals.settings.descriptions.gender')}
|
||||
label={t('modals.settings.labels.gender')}
|
||||
open={genderOpen}
|
||||
onOpenChange={() => openSelect('gender')}
|
||||
onChange={handleGenderChange}
|
||||
onClose={() => setGenderOpen(false)}
|
||||
value={`${gender}`}
|
||||
>
|
||||
<SelectItem key="gran" value="0">
|
||||
{t('modals.settings.gender.gran')}
|
||||
</SelectItem>
|
||||
<SelectItem key="djeeta" value="1">
|
||||
{t('modals.settings.gender.djeeta')}
|
||||
</SelectItem>
|
||||
</SelectTableField>
|
||||
)
|
||||
|
||||
const languageField = () => (
|
||||
<SelectTableField
|
||||
name="language"
|
||||
label={t('modals.settings.labels.language')}
|
||||
open={languageOpen}
|
||||
onOpenChange={() => openSelect('language')}
|
||||
onChange={handleLanguageChange}
|
||||
onClose={() => setLanguageOpen(false)}
|
||||
value={language}
|
||||
>
|
||||
<SelectItem key="en" value="en">
|
||||
{t('modals.settings.language.english')}
|
||||
</SelectItem>
|
||||
<SelectItem key="ja" value="ja">
|
||||
{t('modals.settings.language.japanese')}
|
||||
</SelectItem>
|
||||
</SelectTableField>
|
||||
)
|
||||
|
||||
const themeField = () => (
|
||||
<SelectTableField
|
||||
name="theme"
|
||||
label={t('modals.settings.labels.theme')}
|
||||
open={themeOpen}
|
||||
onOpenChange={() => openSelect('theme')}
|
||||
onChange={handleThemeChange}
|
||||
onClose={() => setThemeOpen(false)}
|
||||
value={theme}
|
||||
>
|
||||
<SelectItem key="system" value="system">
|
||||
{t('modals.settings.theme.system')}
|
||||
</SelectItem>
|
||||
<SelectItem key="light" value="light">
|
||||
{t('modals.settings.theme.light')}
|
||||
</SelectItem>
|
||||
<SelectItem key="dark" value="dark">
|
||||
{t('modals.settings.theme.dark')}
|
||||
</SelectItem>
|
||||
</SelectTableField>
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!mounted) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={openChange}>
|
||||
<DialogContent
|
||||
className="Account"
|
||||
headerref={headerRef}
|
||||
footerref={footerRef}
|
||||
onOpenAutoFocus={(event: Event) => {}}
|
||||
onEscapeKeyDown={onEscapeKeyDown}
|
||||
>
|
||||
<div className="DialogHeader" ref={headerRef}>
|
||||
<div className="DialogTop">
|
||||
<DialogTitle className="SubTitle">
|
||||
{t('modals.settings.title')}
|
||||
</DialogTitle>
|
||||
<DialogTitle className="DialogTitle">@{username}</DialogTitle>
|
||||
</div>
|
||||
<DialogClose className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</DialogClose>
|
||||
</div>
|
||||
<div className="DialogFooter" ref={footerRef}>
|
||||
<Button
|
||||
contained={true}
|
||||
text={t('modals.settings.buttons.confirm')}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
<form onSubmit={update}>
|
||||
<div className="Fields">
|
||||
{pictureField()}
|
||||
{genderField()}
|
||||
{languageField()}
|
||||
{themeField()}
|
||||
</div>
|
||||
<div className="DialogFooter" ref={footerRef}>
|
||||
<Button
|
||||
contained={true}
|
||||
text={t('modals.settings.buttons.confirm')}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default AccountModal
|
||||
|
|
|
|||
|
|
@ -23,7 +23,11 @@ const Alert = (props: Props) => {
|
|||
<AlertDialog.Overlay className="Overlay" onClick={props.cancelAction} />
|
||||
<div className="AlertWrapper">
|
||||
<AlertDialog.Content className="Alert">
|
||||
{props.title ? <AlertDialog.Title>Error</AlertDialog.Title> : ''}
|
||||
{props.title ? (
|
||||
<AlertDialog.Title>{props.title}</AlertDialog.Title>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<AlertDialog.Description className="description">
|
||||
{props.message}
|
||||
</AlertDialog.Description>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,15 @@
|
|||
background: transparent;
|
||||
}
|
||||
|
||||
&.IconButton.medium {
|
||||
height: inherit;
|
||||
padding: $unit-half;
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.Contained {
|
||||
background: var(--button-contained-bg);
|
||||
|
||||
|
|
@ -43,7 +52,7 @@
|
|||
stroke: #ff4d4d;
|
||||
}
|
||||
|
||||
&.Active.Save {
|
||||
&.Save {
|
||||
color: #ff4d4d;
|
||||
|
||||
.Accessory svg {
|
||||
|
|
@ -99,24 +108,27 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.save:hover {
|
||||
color: #ff4d4d;
|
||||
|
||||
&.Save {
|
||||
.Accessory svg {
|
||||
fill: #ff4d4d;
|
||||
stroke: #ff4d4d;
|
||||
fill: none;
|
||||
stroke: var(--button-text);
|
||||
}
|
||||
}
|
||||
|
||||
&.save.Active {
|
||||
color: #ff4d4d;
|
||||
&.Saved {
|
||||
color: #ff4d4d;
|
||||
|
||||
.Accessory svg {
|
||||
fill: #ff4d4d;
|
||||
stroke: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: darken(#ff4d4d, 30);
|
||||
color: #ff4d4d;
|
||||
|
||||
.icon svg {
|
||||
fill: darken(#ff4d4d, 30);
|
||||
stroke: darken(#ff4d4d, 30);
|
||||
.Accessory svg {
|
||||
fill: none;
|
||||
stroke: #ff4d4d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -138,6 +150,10 @@
|
|||
|
||||
display: flex;
|
||||
|
||||
&.Arrow {
|
||||
margin-top: $unit-half;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--button-text);
|
||||
height: $dimension;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ interface Props
|
|||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
> {
|
||||
accessoryIcon?: React.ReactNode
|
||||
leftAccessoryIcon?: React.ReactNode
|
||||
leftAccessoryClassName?: string
|
||||
rightAccessoryIcon?: React.ReactNode
|
||||
rightAccessoryClassName?: string
|
||||
active?: boolean
|
||||
blended?: boolean
|
||||
contained?: boolean
|
||||
|
|
@ -24,22 +27,45 @@ const defaultProps = {
|
|||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, Props>(function button(
|
||||
{ accessoryIcon, active, blended, contained, buttonSize, text, ...props },
|
||||
{
|
||||
leftAccessoryIcon,
|
||||
leftAccessoryClassName,
|
||||
rightAccessoryIcon,
|
||||
rightAccessoryClassName,
|
||||
active,
|
||||
blended,
|
||||
contained,
|
||||
buttonSize,
|
||||
text,
|
||||
...props
|
||||
},
|
||||
forwardedRef
|
||||
) {
|
||||
const classes = classNames(
|
||||
{
|
||||
Button: true,
|
||||
Active: active,
|
||||
Blended: blended,
|
||||
Contained: contained,
|
||||
},
|
||||
buttonSize,
|
||||
props.className
|
||||
)
|
||||
const classes = classNames(buttonSize, props.className, {
|
||||
Button: true,
|
||||
Active: active,
|
||||
Blended: blended,
|
||||
Contained: contained,
|
||||
})
|
||||
|
||||
const hasAccessory = () => {
|
||||
if (accessoryIcon) return <span className="Accessory">{accessoryIcon}</span>
|
||||
const leftAccessoryClasses = classNames(leftAccessoryClassName, {
|
||||
Accessory: true,
|
||||
Left: true,
|
||||
})
|
||||
|
||||
const rightAccessoryClasses = classNames(rightAccessoryClassName, {
|
||||
Accessory: true,
|
||||
Right: true,
|
||||
})
|
||||
|
||||
const hasLeftAccessory = () => {
|
||||
if (leftAccessoryIcon)
|
||||
return <span className={leftAccessoryClasses}>{leftAccessoryIcon}</span>
|
||||
}
|
||||
|
||||
const hasRightAccessory = () => {
|
||||
if (rightAccessoryIcon)
|
||||
return <span className={rightAccessoryClasses}>{rightAccessoryIcon}</span>
|
||||
}
|
||||
|
||||
const hasText = () => {
|
||||
|
|
@ -48,8 +74,9 @@ const Button = React.forwardRef<HTMLButtonElement, Props>(function button(
|
|||
|
||||
return (
|
||||
<button {...props} className={classes} ref={forwardedRef}>
|
||||
{hasAccessory()}
|
||||
{hasLeftAccessory()}
|
||||
{hasText()}
|
||||
{hasRightAccessory()}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,166 +0,0 @@
|
|||
import React from 'react'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import ChangelogUnit from '~components/ChangelogUnit'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '~components/Dialog'
|
||||
import DialogContent from '~components/DialogContent'
|
||||
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
const ChangelogModal = () => {
|
||||
const { t } = useTranslation('common')
|
||||
const headerRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t('modals.changelog.title')}</span>
|
||||
</li>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="Changelog"
|
||||
title={t('menu.changelog')}
|
||||
headerref={headerRef}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onEscapeKeyDown={() => {}}
|
||||
>
|
||||
<div className="DialogHeader" ref={headerRef}>
|
||||
<DialogTitle className="DialogTitle">
|
||||
{t('menu.changelog')}
|
||||
</DialogTitle>
|
||||
<DialogClose className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</DialogClose>
|
||||
</div>
|
||||
|
||||
<div className="updates">
|
||||
<section className="version" data-version="1.0">
|
||||
<div className="top">
|
||||
<h3>1.0.1</h3>
|
||||
<time>2023/01/08</time>
|
||||
</div>
|
||||
<ul className="notes">
|
||||
<li>Extra party fields: Full Auto, Clear Time, and more</li>
|
||||
<li>Support for Youtube short URLs</li>
|
||||
<li>Responsive grids and lots of other mobile fixes</li>
|
||||
<li>Many other bug fixes</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section className="content version" data-version="2022-12L">
|
||||
<div className="top">
|
||||
<h3>2022-12 Legend Festival</h3>
|
||||
<time>2022/12/26</time>
|
||||
</div>
|
||||
<div className="update">
|
||||
<section className="characters">
|
||||
<h4>New characters</h4>
|
||||
<div className="items">
|
||||
<ChangelogUnit
|
||||
name="Michael (Grand)"
|
||||
id="3040440000"
|
||||
type="character"
|
||||
/>
|
||||
<ChangelogUnit
|
||||
name="Makura"
|
||||
id="3040441000"
|
||||
type="character"
|
||||
/>
|
||||
<ChangelogUnit
|
||||
name="Ultimate Friday"
|
||||
id="3040442000"
|
||||
type="character"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section className="weapons">
|
||||
<h4>New weapons</h4>
|
||||
<div className="items">
|
||||
<ChangelogUnit
|
||||
name="Crimson Scale"
|
||||
id="1040315900"
|
||||
type="weapon"
|
||||
/>
|
||||
<ChangelogUnit
|
||||
name="Leporidius"
|
||||
id="1040914500"
|
||||
type="weapon"
|
||||
/>
|
||||
<ChangelogUnit
|
||||
name="FRIED Spear"
|
||||
id="1040218200"
|
||||
type="weapon"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section className="summons">
|
||||
<h4>New summons</h4>
|
||||
<div className="items">
|
||||
<ChangelogUnit name="Yatima" id="2040417000" type="summon" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<section className="content version" data-version="2022-12F2">
|
||||
<div className="top">
|
||||
<h3>2022-12 Flash Gala</h3>
|
||||
<time>2022/12/26</time>
|
||||
</div>
|
||||
<div className="update">
|
||||
<section className="characters">
|
||||
<h4>New characters</h4>
|
||||
<div className="items">
|
||||
<ChangelogUnit
|
||||
name="Charlotta (Grand)"
|
||||
id="3040438000"
|
||||
type="character"
|
||||
/>
|
||||
<ChangelogUnit name="Erin" id="3040439000" type="character" />
|
||||
</div>
|
||||
</section>
|
||||
<section className="weapons">
|
||||
<h4>New weapons</h4>
|
||||
<div className="items">
|
||||
<ChangelogUnit
|
||||
name="Claíomh Solais Díon"
|
||||
id="1040024200"
|
||||
type="weapon"
|
||||
/>
|
||||
<ChangelogUnit
|
||||
name="Crystal Edge"
|
||||
id="1040116500"
|
||||
type="weapon"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<section className="version" data-version="1.0">
|
||||
<div className="top">
|
||||
<h3>1.0.0</h3>
|
||||
<time>2022/12/26</time>
|
||||
</div>
|
||||
<ul className="notes">
|
||||
<li>First release!</li>
|
||||
<li>You can embed Youtube videos now</li>
|
||||
<li>Better clicking - right-click and open in a new tab</li>
|
||||
<li>Manually set dark mode in Account Settings</li>
|
||||
<li>Lots of bugs squashed</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChangelogModal
|
||||
|
|
@ -2,8 +2,9 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { getCookie } from 'cookies-next'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { AxiosError, AxiosResponse } from 'axios'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import Alert from '~components/Alert'
|
||||
|
|
@ -31,12 +32,19 @@ const CharacterGrid = (props: Props) => {
|
|||
// Constants
|
||||
const numCharacters: number = 5
|
||||
|
||||
// Localization
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Cookies
|
||||
const cookie = getCookie('account')
|
||||
const accountData: AccountCookie = cookie
|
||||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
|
||||
// Set up state for error handling
|
||||
const [axiosError, setAxiosError] = useState<AxiosResponse>()
|
||||
const [errorAlertOpen, setErrorAlertOpen] = useState(false)
|
||||
|
||||
// Set up state for view management
|
||||
const { party, grid } = useSnapshot(appState)
|
||||
const [slug, setSlug] = useState()
|
||||
|
|
@ -55,6 +63,7 @@ const CharacterGrid = (props: Props) => {
|
|||
2: undefined,
|
||||
3: undefined,
|
||||
})
|
||||
const [jobAccessory, setJobAccessory] = useState<JobAccessory>()
|
||||
const [errorMessage, setErrorMessage] = useState('')
|
||||
|
||||
// Create a temporary state to store previous weapon uncap values and transcendence stages
|
||||
|
|
@ -81,6 +90,7 @@ const CharacterGrid = (props: Props) => {
|
|||
useEffect(() => {
|
||||
setJob(appState.party.job)
|
||||
setJobSkills(appState.party.jobSkills)
|
||||
setJobAccessory(appState.party.accessory)
|
||||
}, [appState])
|
||||
|
||||
// Initialize an array of current uncap values for each characters
|
||||
|
|
@ -109,7 +119,15 @@ const CharacterGrid = (props: Props) => {
|
|||
if (party.editable)
|
||||
saveCharacter(party.id, character, position)
|
||||
.then((response) => handleCharacterResponse(response.data))
|
||||
.catch((error) => console.error(error))
|
||||
.catch((error) => {
|
||||
const axiosError = error as AxiosError
|
||||
const response = axiosError.response
|
||||
|
||||
if (response) {
|
||||
setErrorAlertOpen(true)
|
||||
setAxiosError(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -186,7 +204,7 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
|
||||
// Methods: Saving job and job skills
|
||||
const saveJob = async function (job?: Job) {
|
||||
async function saveJob(job?: Job) {
|
||||
const payload = {
|
||||
party: {
|
||||
job_id: job ? job.id : -1,
|
||||
|
|
@ -214,7 +232,7 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
const saveJobSkill = function (skill: JobSkill, position: number) {
|
||||
function saveJobSkill(skill: JobSkill, position: number) {
|
||||
if (party.id && appState.party.editable) {
|
||||
const positionedKey = `skill${position}_id`
|
||||
|
||||
|
|
@ -253,6 +271,24 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
async function saveAccessory(accessory: JobAccessory) {
|
||||
const payload = {
|
||||
party: {
|
||||
accessory_id: accessory.id,
|
||||
},
|
||||
}
|
||||
|
||||
if (appState.party.id) {
|
||||
const response = await api.endpoints.parties.update(
|
||||
appState.party.id,
|
||||
payload
|
||||
)
|
||||
const team = response.data.party
|
||||
setJobAccessory(team.accessory)
|
||||
appState.party.accessory = team.accessory
|
||||
}
|
||||
}
|
||||
|
||||
// Methods: Helpers
|
||||
function characterUncapLevel(character: Character) {
|
||||
let uncapLevel
|
||||
|
|
@ -462,6 +498,18 @@ const CharacterGrid = (props: Props) => {
|
|||
}
|
||||
|
||||
// Render: JSX components
|
||||
const errorAlert = () => {
|
||||
return (
|
||||
<Alert
|
||||
open={errorAlertOpen}
|
||||
title={axiosError ? `${axiosError.status}` : 'Error'}
|
||||
message={t(`errors.${axiosError?.statusText.toLowerCase()}`)}
|
||||
cancelAction={() => setErrorAlertOpen(false)}
|
||||
cancelActionText={t('buttons.confirm')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Alert
|
||||
|
|
@ -474,9 +522,11 @@ const CharacterGrid = (props: Props) => {
|
|||
<JobSection
|
||||
job={job}
|
||||
jobSkills={jobSkills}
|
||||
jobAccessory={jobAccessory}
|
||||
editable={party.editable}
|
||||
saveJob={saveJob}
|
||||
saveSkill={saveJobSkill}
|
||||
saveAccessory={saveAccessory}
|
||||
/>
|
||||
<CharacterConflictModal
|
||||
open={modalOpen}
|
||||
|
|
@ -504,6 +554,7 @@ const CharacterGrid = (props: Props) => {
|
|||
})}
|
||||
</ul>
|
||||
</div>
|
||||
{errorAlert()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ const CharacterUnit = ({
|
|||
|
||||
function removeCharacter() {
|
||||
if (gridCharacter) sendCharacterToRemove(gridCharacter.id)
|
||||
setAlertOpen(false)
|
||||
}
|
||||
|
||||
// Methods: Image string generation
|
||||
|
|
@ -218,7 +219,7 @@ const CharacterUnit = ({
|
|||
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<Button
|
||||
accessoryIcon={<SettingsIcon />}
|
||||
leftAccessoryIcon={<SettingsIcon />}
|
||||
className={buttonClasses}
|
||||
onClick={handleButtonClicked}
|
||||
/>
|
||||
|
|
|
|||
188
components/DropdownMenuContent/index.scss
Normal file
188
components/DropdownMenuContent/index.scss
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
.Menu {
|
||||
transform-origin: --radix-dropdown-menu-content-transform-origin;
|
||||
background: var(--menu-bg);
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 8%);
|
||||
box-sizing: border-box;
|
||||
min-width: 15vw;
|
||||
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;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
left: $unit-2x;
|
||||
right: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.MenuItem {
|
||||
color: var(--text-tertiary);
|
||||
font-weight: $normal;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background: var(--menu-bg-item-hover);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
color: var(--text-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:visited {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(phone) {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
cursor: default;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.profile > div {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
&.language {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
padding-right: $unit;
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.Switch {
|
||||
$height: 24px;
|
||||
|
||||
background: $grey-60;
|
||||
border-radius: calc($height / 2);
|
||||
border: none;
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: $height;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Thumb {
|
||||
$diameter: 18px;
|
||||
|
||||
background: $grey-100;
|
||||
border-radius: calc($diameter / 2);
|
||||
display: block;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
z-index: 3;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-state='checked'] {
|
||||
background: $grey-100;
|
||||
left: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
color: $grey-100;
|
||||
font-size: 10px;
|
||||
font-weight: $bold;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.left {
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.right {
|
||||
top: 6px;
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $grey-50;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:visited {
|
||||
color: $grey-50;
|
||||
}
|
||||
}
|
||||
|
||||
& > a,
|
||||
& > span {
|
||||
display: block;
|
||||
padding: 12px 12px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 10px 12px;
|
||||
|
||||
&:hover {
|
||||
i.tag {
|
||||
background: var(--tag-bg);
|
||||
color: var(--tag-text);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
img {
|
||||
$diameter: 32px;
|
||||
border-radius: calc($diameter / 2);
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.MenuGroup {
|
||||
border-bottom: 1px solid var(--menu-separator);
|
||||
|
||||
&:first-child .MenuItem:first-child:hover {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
|
||||
&:last-child .MenuItem:last-child:hover {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
37
components/DropdownMenuContent/index.tsx
Normal file
37
components/DropdownMenuContent/index.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import React, { PropsWithChildren } from 'react'
|
||||
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props extends DropdownMenuPrimitive.DropdownMenuContentProps {}
|
||||
|
||||
export const DropdownMenu = DropdownMenuPrimitive.Root
|
||||
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
|
||||
export const DropdownMenuLabel = DropdownMenuPrimitive.Label
|
||||
export const DropdownMenuItem = DropdownMenuPrimitive.Item
|
||||
export const DropdownMenuGroup = DropdownMenuPrimitive.Group
|
||||
export const DropdownMenuSeparator = DropdownMenuPrimitive.Separator
|
||||
|
||||
export const DropdownMenuContent = React.forwardRef<HTMLDivElement, Props>(
|
||||
({ children, ...props }: PropsWithChildren<Props>, forwardedRef) => {
|
||||
const classes = classNames(props.className, {
|
||||
Menu: true,
|
||||
})
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
{...props}
|
||||
className={classes}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.Content>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
DropdownMenuContent.defaultProps = {
|
||||
sideOffset: 4,
|
||||
}
|
||||
|
|
@ -208,7 +208,7 @@ const GridRep = (props: Props) => {
|
|||
<a href="#">
|
||||
<Button
|
||||
className="Save"
|
||||
accessoryIcon={<SaveIcon className="stroke" />}
|
||||
leftAccessoryIcon={<SaveIcon className="stroke" />}
|
||||
active={props.favorited}
|
||||
contained={true}
|
||||
buttonSize="small"
|
||||
|
|
|
|||
|
|
@ -5,11 +5,23 @@
|
|||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
#Right > div {
|
||||
section {
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
}
|
||||
|
||||
img,
|
||||
.placeholder {
|
||||
$diameter: 32px;
|
||||
border-radius: calc($diameter / 2);
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
background: var(--placeholder-bg);
|
||||
}
|
||||
|
||||
#DropdownWrapper {
|
||||
display: inline-block;
|
||||
padding-bottom: $unit;
|
||||
|
|
@ -20,7 +32,7 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
padding-right: $unit-4x;
|
||||
// padding-right: $unit-4x;
|
||||
|
||||
.Button {
|
||||
background: var(--button-bg-hover);
|
||||
|
|
|
|||
|
|
@ -3,23 +3,37 @@ import { useSnapshot } from 'valtio'
|
|||
import { deleteCookie } from 'cookies-next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import classNames from 'classnames'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
import Link from 'next/link'
|
||||
|
||||
import api from '~utils/api'
|
||||
import { accountState, initialAccountState } from '~utils/accountState'
|
||||
import { appState, initialAppState } from '~utils/appState'
|
||||
import { appState } from '~utils/appState'
|
||||
import capitalizeFirstLetter from '~utils/capitalizeFirstLetter'
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
} from '~components/DropdownMenuContent'
|
||||
import LoginModal from '~components/LoginModal'
|
||||
import SignupModal from '~components/SignupModal'
|
||||
import AccountModal from '~components/AccountModal'
|
||||
import Toast from '~components/Toast'
|
||||
import Button from '~components/Button'
|
||||
import HeaderMenu from '~components/HeaderMenu'
|
||||
|
||||
import AddIcon from '~public/icons/Add.svg'
|
||||
import ArrowIcon from '~public/icons/Arrow.svg'
|
||||
import LinkIcon from '~public/icons/Link.svg'
|
||||
import MenuIcon from '~public/icons/Menu.svg'
|
||||
import RemixIcon from '~public/icons/Remix.svg'
|
||||
import SaveIcon from '~public/icons/Save.svg'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import './index.scss'
|
||||
import Tooltip from '~components/Tooltip'
|
||||
|
||||
const Header = () => {
|
||||
// Localization
|
||||
|
|
@ -29,18 +43,46 @@ const Header = () => {
|
|||
const router = useRouter()
|
||||
|
||||
// State management
|
||||
const [open, setOpen] = useState(false)
|
||||
const [copyToastOpen, setCopyToastOpen] = useState(false)
|
||||
const [loginModalOpen, setLoginModalOpen] = useState(false)
|
||||
const [signupModalOpen, setSignupModalOpen] = useState(false)
|
||||
const [settingsModalOpen, setSettingsModalOpen] = useState(false)
|
||||
const [leftMenuOpen, setLeftMenuOpen] = useState(false)
|
||||
const [rightMenuOpen, setRightMenuOpen] = useState(false)
|
||||
|
||||
// Snapshots
|
||||
const { account } = useSnapshot(accountState)
|
||||
const { party } = useSnapshot(appState)
|
||||
|
||||
function menuButtonClicked() {
|
||||
setOpen(!open)
|
||||
function handleCopyToastOpenChanged(open: boolean) {
|
||||
setCopyToastOpen(open)
|
||||
}
|
||||
|
||||
function onClickOutsideMenu() {
|
||||
setOpen(false)
|
||||
function handleCopyToastCloseClicked() {
|
||||
setCopyToastOpen(false)
|
||||
}
|
||||
|
||||
function handleLeftMenuButtonClicked() {
|
||||
setLeftMenuOpen(!leftMenuOpen)
|
||||
}
|
||||
|
||||
function handleRightMenuButtonClicked() {
|
||||
setRightMenuOpen(!rightMenuOpen)
|
||||
}
|
||||
|
||||
function handleLeftMenuOpenChange(open: boolean) {
|
||||
setLeftMenuOpen(open)
|
||||
}
|
||||
function handleRightMenuOpenChange(open: boolean) {
|
||||
setRightMenuOpen(open)
|
||||
}
|
||||
|
||||
function closeLeftMenu() {
|
||||
setLeftMenuOpen(false)
|
||||
}
|
||||
|
||||
function closeRightMenu() {
|
||||
setRightMenuOpen(false)
|
||||
}
|
||||
|
||||
function copyToClipboard() {
|
||||
|
|
@ -52,23 +94,25 @@ const Header = () => {
|
|||
el.select()
|
||||
document.execCommand('copy')
|
||||
el.remove()
|
||||
|
||||
setCopyToastOpen(true)
|
||||
}
|
||||
|
||||
function newParty() {
|
||||
function handleNewParty(event: React.MouseEvent, path: string) {
|
||||
event.preventDefault()
|
||||
|
||||
// Push the root URL
|
||||
router.push('/')
|
||||
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
|
||||
closeRightMenu()
|
||||
}
|
||||
|
||||
function logout() {
|
||||
// Close menu
|
||||
closeRightMenu()
|
||||
|
||||
// Delete cookies
|
||||
deleteCookie('account')
|
||||
deleteCookie('user')
|
||||
|
||||
|
|
@ -103,85 +147,329 @@ const Header = () => {
|
|||
else console.error('Failed to unsave team: No party ID')
|
||||
}
|
||||
|
||||
const copyButton = () => {
|
||||
if (router.route === '/p/[party]')
|
||||
return (
|
||||
<Button
|
||||
accessoryIcon={<LinkIcon className="stroke" />}
|
||||
blended={true}
|
||||
text={t('buttons.copy')}
|
||||
onClick={copyToClipboard}
|
||||
/>
|
||||
)
|
||||
function remixTeam() {
|
||||
if (party.shortcode)
|
||||
api.remix(party.shortcode).then((response) => {
|
||||
const remix = response.data.party
|
||||
router.push(`/p/${remix.shortcode}`)
|
||||
})
|
||||
}
|
||||
|
||||
const leftNav = () => {
|
||||
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 if (['weapons', 'summons', 'characters', 'new', ''].includes(path)) {
|
||||
title = t('new_party')
|
||||
} else {
|
||||
title = ''
|
||||
}
|
||||
|
||||
return title !== '' ? (
|
||||
<Button
|
||||
blended={true}
|
||||
rightAccessoryIcon={
|
||||
path === 'p' && hasAccessory ? (
|
||||
<LinkIcon className="stroke" />
|
||||
) : undefined
|
||||
}
|
||||
text={title}
|
||||
onClick={copyToClipboard}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
const profileImage = () => {
|
||||
let image
|
||||
|
||||
const user = accountState.account.user
|
||||
if (accountState.account.authorized && user) {
|
||||
image = (
|
||||
<img
|
||||
alt={user.username}
|
||||
className={`profile ${user.avatar.element}`}
|
||||
srcSet={`/profile/${user.avatar.picture}.png,
|
||||
/profile/${user.avatar.picture}@2x.png 2x`}
|
||||
src={`/profile/${user.avatar.picture}.png`}
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
image = <div className="profile placeholder" />
|
||||
}
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
const urlCopyToast = () => {
|
||||
return (
|
||||
<div id="DropdownWrapper">
|
||||
<Button
|
||||
accessoryIcon={<MenuIcon />}
|
||||
className={classNames({ Active: open })}
|
||||
blended={true}
|
||||
text={t('buttons.menu')}
|
||||
onClick={menuButtonClicked}
|
||||
/>
|
||||
<HeaderMenu
|
||||
authenticated={account.authorized}
|
||||
open={open}
|
||||
username={account.user?.username}
|
||||
onClickOutside={onClickOutsideMenu}
|
||||
logout={logout}
|
||||
/>
|
||||
</div>
|
||||
<Toast
|
||||
open={copyToastOpen}
|
||||
duration={2400}
|
||||
type="foreground"
|
||||
content={t('toasts.copied')}
|
||||
onOpenChange={handleCopyToastOpenChanged}
|
||||
onCloseClick={handleCopyToastCloseClicked}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const saveButton = () => {
|
||||
if (party.favorited)
|
||||
return (
|
||||
return (
|
||||
<Tooltip content={t('tooltips.save')}>
|
||||
<Button
|
||||
accessoryIcon={<SaveIcon />}
|
||||
leftAccessoryIcon={<SaveIcon />}
|
||||
className={classNames({
|
||||
Save: true,
|
||||
Saved: party.favorited,
|
||||
})}
|
||||
blended={true}
|
||||
text="Saved"
|
||||
text={party.favorited ? t('buttons.saved') : t('buttons.save')}
|
||||
onClick={toggleFavorite}
|
||||
/>
|
||||
)
|
||||
else
|
||||
return (
|
||||
<Button
|
||||
accessoryIcon={<SaveIcon />}
|
||||
blended={true}
|
||||
text="Save"
|
||||
onClick={toggleFavorite}
|
||||
/>
|
||||
)
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const rightNav = () => {
|
||||
const remixButton = () => {
|
||||
return (
|
||||
<div>
|
||||
<Tooltip content={t('tooltips.remix')}>
|
||||
<Button
|
||||
leftAccessoryIcon={<RemixIcon />}
|
||||
className="Remix"
|
||||
blended={true}
|
||||
text={t('buttons.remix')}
|
||||
onClick={remixTeam}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const settingsModal = () => {
|
||||
const user = accountState.account.user
|
||||
|
||||
if (user) {
|
||||
return (
|
||||
<AccountModal
|
||||
open={settingsModalOpen}
|
||||
username={user.username}
|
||||
picture={user.avatar.picture}
|
||||
gender={user.gender}
|
||||
language={user.language}
|
||||
theme={user.theme}
|
||||
onOpenChange={setSettingsModalOpen}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const loginModal = () => {
|
||||
return <LoginModal open={loginModalOpen} onOpenChange={setLoginModalOpen} />
|
||||
}
|
||||
|
||||
const signupModal = () => {
|
||||
return (
|
||||
<SignupModal open={signupModalOpen} onOpenChange={setSignupModalOpen} />
|
||||
)
|
||||
}
|
||||
|
||||
const left = () => {
|
||||
return (
|
||||
<section>
|
||||
<div id="DropdownWrapper">
|
||||
<DropdownMenu
|
||||
open={leftMenuOpen}
|
||||
onOpenChange={handleLeftMenuOpenChange}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
leftAccessoryIcon={<MenuIcon />}
|
||||
className={classNames({ Active: leftMenuOpen })}
|
||||
blended={true}
|
||||
onClick={handleLeftMenuButtonClicked}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="Left">
|
||||
{leftMenuItems()}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
{pageTitle()}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const right = () => {
|
||||
return (
|
||||
<section>
|
||||
{router.route === '/p/[party]' &&
|
||||
account.user &&
|
||||
(!party.user || party.user.id !== account.user.id)
|
||||
? saveButton()
|
||||
: ''}
|
||||
|
||||
{copyButton()}
|
||||
|
||||
<Button
|
||||
accessoryIcon={<AddIcon className="Add" />}
|
||||
blended={true}
|
||||
text={t('buttons.new')}
|
||||
onClick={newParty}
|
||||
/>
|
||||
</div>
|
||||
{router.route === '/p/[party]' ? remixButton() : ''}
|
||||
<DropdownMenu
|
||||
open={rightMenuOpen}
|
||||
onOpenChange={handleRightMenuOpenChange}
|
||||
>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className={classNames({ Active: rightMenuOpen })}
|
||||
leftAccessoryIcon={profileImage()}
|
||||
rightAccessoryIcon={<ArrowIcon />}
|
||||
rightAccessoryClassName="Arrow"
|
||||
onClick={handleRightMenuButtonClicked}
|
||||
blended={true}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="Right">
|
||||
{rightMenuItems()}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
const leftMenuItems = () => {
|
||||
return (
|
||||
<>
|
||||
{accountState.account.authorized && accountState.account.user ? (
|
||||
<>
|
||||
<DropdownMenuGroup className="MenuGroup">
|
||||
<DropdownMenuItem className="MenuItem" onClick={closeRightMenu}>
|
||||
<Link
|
||||
href={`/${accountState.account.user.username}` || ''}
|
||||
passHref
|
||||
>
|
||||
Your profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
|
||||
<Link href={`/saved` || ''}>{t('menu.saved')}</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<DropdownMenuGroup className="MenuGroup">
|
||||
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
|
||||
<Link href="/teams">{t('menu.teams')}</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="MenuItem">
|
||||
<div>
|
||||
<span>{t('menu.guides')}</span>
|
||||
<i className="tag">{t('coming_soon')}</i>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuGroup className="MenuGroup">
|
||||
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
|
||||
<a href="/about" target="_blank">
|
||||
{t('about.segmented_control.about')}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
|
||||
<a href="/updates" target="_blank">
|
||||
{t('about.segmented_control.updates')}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="MenuItem" onClick={closeLeftMenu}>
|
||||
<a href="/roadmap" target="_blank">
|
||||
{t('about.segmented_control.roadmap')}
|
||||
</a>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const rightMenuItems = () => {
|
||||
let items
|
||||
|
||||
const account = accountState.account
|
||||
if (account.authorized && account.user) {
|
||||
items = (
|
||||
<>
|
||||
<DropdownMenuGroup className="MenuGroup">
|
||||
<DropdownMenuItem className="MenuItem">
|
||||
<Link href="/new">
|
||||
<a onClick={(e: React.MouseEvent) => handleNewParty(e, '/new')}>
|
||||
New party
|
||||
</a>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="MenuItem">
|
||||
<Link href={`/${account.user.username}` || ''} passHref>
|
||||
Your profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup className="MenuGroup">
|
||||
<DropdownMenuItem
|
||||
className="MenuItem"
|
||||
onClick={() => setSettingsModalOpen(true)}
|
||||
>
|
||||
<span>{t('menu.settings')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="MenuItem" onClick={logout}>
|
||||
<span>{t('menu.logout')}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
items = (
|
||||
<>
|
||||
<DropdownMenuGroup className="MenuGroup">
|
||||
<DropdownMenuItem className="MenuItem">
|
||||
<Link href="/new">
|
||||
<a onClick={(e: React.MouseEvent) => handleNewParty(e, '/new')}>
|
||||
New party
|
||||
</a>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup className="MenuGroup">
|
||||
<DropdownMenuItem
|
||||
className="MenuItem"
|
||||
onClick={() => setLoginModalOpen(true)}
|
||||
>
|
||||
<span>Log in</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="MenuItem"
|
||||
onClick={() => setSignupModalOpen(true)}
|
||||
>
|
||||
<span>Sign up</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
return (
|
||||
<nav id="Header">
|
||||
<div id="Left">{leftNav()}</div>
|
||||
<div id="Right">{rightNav()}</div>
|
||||
{left()}
|
||||
{right()}
|
||||
{urlCopyToast()}
|
||||
{settingsModal()}
|
||||
{loginModal()}
|
||||
{signupModal()}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,187 +0,0 @@
|
|||
.Menu {
|
||||
background: var(--menu-bg);
|
||||
border-radius: 6px;
|
||||
box-sizing: border-box;
|
||||
display: none;
|
||||
min-width: 220px;
|
||||
position: absolute;
|
||||
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: 10;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
left: $unit-2x;
|
||||
right: $unit-2x;
|
||||
}
|
||||
}
|
||||
|
||||
.MenuItem {
|
||||
color: var(--text-tertiary);
|
||||
font-weight: $normal;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover:not(.disabled) {
|
||||
background: var(--menu-bg-item-hover);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
|
||||
a {
|
||||
color: var(--text-primary);
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:visited {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@include breakpoint(phone) {
|
||||
background: inherit;
|
||||
color: inherit;
|
||||
cursor: default;
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.profile > div {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
&.language {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: $unit;
|
||||
padding-right: $unit;
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.Switch {
|
||||
$height: 24px;
|
||||
|
||||
background: $grey-60;
|
||||
border-radius: calc($height / 2);
|
||||
border: none;
|
||||
position: relative;
|
||||
width: 44px;
|
||||
height: $height;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.Thumb {
|
||||
$diameter: 18px;
|
||||
|
||||
background: $grey-100;
|
||||
border-radius: calc($diameter / 2);
|
||||
display: block;
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
z-index: 3;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-state='checked'] {
|
||||
background: $grey-100;
|
||||
left: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
.left,
|
||||
.right {
|
||||
color: $grey-100;
|
||||
font-size: 10px;
|
||||
font-weight: $bold;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.left {
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
}
|
||||
|
||||
.right {
|
||||
top: 6px;
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
color: $grey-50;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:visited {
|
||||
color: $grey-50;
|
||||
}
|
||||
}
|
||||
|
||||
& > a,
|
||||
& > span {
|
||||
display: block;
|
||||
padding: 12px 12px;
|
||||
}
|
||||
|
||||
& > div {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 10px 12px;
|
||||
|
||||
&:hover {
|
||||
i.tag {
|
||||
background: var(--tag-bg);
|
||||
color: var(--tag-text);
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
img {
|
||||
$diameter: 32px;
|
||||
border-radius: calc($diameter / 2);
|
||||
height: $diameter;
|
||||
width: $diameter;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.MenuGroup {
|
||||
border-bottom: 1px solid var(--menu-separator);
|
||||
|
||||
&:first-child .MenuItem:first-child:hover {
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
|
||||
&:last-child .MenuItem:last-child:hover {
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,10 +8,7 @@ import { retrieveCookies, retrieveLocaleCookies } from '~utils/retrieveCookies'
|
|||
import Link from 'next/link'
|
||||
import * as Switch from '@radix-ui/react-switch'
|
||||
|
||||
import AboutModal from '~components/AboutModal'
|
||||
import AccountModal from '~components/AccountModal'
|
||||
import ChangelogModal from '~components/ChangelogModal'
|
||||
import RoadmapModal from '~components/RoadmapModal'
|
||||
import LoginModal from '~components/LoginModal'
|
||||
import SignupModal from '~components/SignupModal'
|
||||
|
||||
|
|
@ -65,7 +62,11 @@ const HeaderMenu = (props: Props) => {
|
|||
|
||||
function handleCheckedChange(value: boolean) {
|
||||
const language = value ? 'ja' : 'en'
|
||||
setCookie('NEXT_LOCALE', language, { path: '/' })
|
||||
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 60)
|
||||
|
||||
setCookie('NEXT_LOCALE', language, { path: '/', expires: expiresAt })
|
||||
router.push(router.asPath, undefined, { locale: language })
|
||||
}
|
||||
|
||||
|
|
@ -110,9 +111,21 @@ const HeaderMenu = (props: Props) => {
|
|||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<AboutModal />
|
||||
<ChangelogModal />
|
||||
<RoadmapModal />
|
||||
<li className="MenuItem">
|
||||
<a href="/about" target="_blank">
|
||||
{t('about.segmented_control.about')}
|
||||
</a>
|
||||
</li>
|
||||
<li className="MenuItem">
|
||||
<a href="/updates" target="_blank">
|
||||
{t('about.segmented_control.updates')}
|
||||
</a>
|
||||
</li>
|
||||
<li className="MenuItem">
|
||||
<a href="/roadmap" target="_blank">
|
||||
{t('about.segmented_control.roadmap')}
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<AccountModal
|
||||
|
|
@ -160,9 +173,21 @@ const HeaderMenu = (props: Props) => {
|
|||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<AboutModal />
|
||||
<ChangelogModal />
|
||||
<RoadmapModal />
|
||||
<li className="MenuItem">
|
||||
<a href="/about" target="_blank">
|
||||
{t('about.segmented_control.about')}
|
||||
</a>
|
||||
</li>
|
||||
<li className="MenuItem">
|
||||
<a href="/updates" target="_blank">
|
||||
{t('about.segmented_control.updates')}
|
||||
</a>
|
||||
</li>
|
||||
<li className="MenuItem">
|
||||
<a href="/roadmap" target="_blank">
|
||||
{t('about.segmented_control.roadmap')}
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
<div className="MenuGroup">
|
||||
<LoginModal />
|
||||
|
|
|
|||
52
components/JobAccessoryItem/index.scss
Normal file
52
components/JobAccessoryItem/index.scss
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
.JobAccessoryItem {
|
||||
background: none;
|
||||
border-radius: $input-corner;
|
||||
border: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
padding: $unit;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
|
||||
&[data-state='checked'] {
|
||||
background: var(--selected-item-bg);
|
||||
|
||||
&:hover {
|
||||
background: var(--selected-item-bg-hover);
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: var(--button-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
background: var(--input-bg-hover);
|
||||
|
||||
img {
|
||||
transform: scale(1.025);
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: var(--button-text-hover);
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: var(--button-text);
|
||||
font-size: $font-small;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: $item-corner;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
transition: $duration-zoom all ease-in-out;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
34
components/JobAccessoryItem/index.tsx
Normal file
34
components/JobAccessoryItem/index.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import React from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
accessory: JobAccessory
|
||||
selected: boolean
|
||||
}
|
||||
|
||||
const JobAccessoryItem = ({ accessory, selected }: Props) => {
|
||||
// Localization
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
return (
|
||||
<RadioGroup.Item
|
||||
className="JobAccessoryItem"
|
||||
data-state={selected ? 'checked' : 'unchecked'}
|
||||
value={accessory.id}
|
||||
>
|
||||
<img
|
||||
alt={accessory.name[locale]}
|
||||
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/accessory-grid/${accessory.granblue_id}.jpg`}
|
||||
/>
|
||||
<h4>{accessory.name[locale]}</h4>
|
||||
</RadioGroup.Item>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobAccessoryItem
|
||||
67
components/JobAccessoryPopover/index.scss
Normal file
67
components/JobAccessoryPopover/index.scss
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
.JobAccessory.Popover {
|
||||
padding: $unit-2x;
|
||||
min-width: 40vw;
|
||||
max-width: 40vw;
|
||||
max-height: 40vh;
|
||||
overflow-y: scroll;
|
||||
margin-left: $unit-2x;
|
||||
|
||||
h3 {
|
||||
font-size: $font-regular;
|
||||
font-weight: $medium;
|
||||
margin: 0 0 $unit $unit;
|
||||
}
|
||||
|
||||
&.ReadOnly {
|
||||
min-width: inherit;
|
||||
max-width: inherit;
|
||||
}
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
width: initial;
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
@include breakpoint(phone) {
|
||||
width: initial;
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
.Accessories {
|
||||
display: grid;
|
||||
gap: $unit;
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
|
||||
gap: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.EquippedAccessory {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.Accessory {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
|
||||
h4 {
|
||||
font-size: $font-small;
|
||||
font-weight: $medium;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img {
|
||||
border-radius: $item-corner;
|
||||
width: 150px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
152
components/JobAccessoryPopover/index.tsx
Normal file
152
components/JobAccessoryPopover/index.tsx
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import capitalizeFirstLetter from '~utils/capitalizeFirstLetter'
|
||||
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import Button from '~components/Button'
|
||||
import {
|
||||
Popover,
|
||||
PopoverTrigger,
|
||||
PopoverContent,
|
||||
} from '~components/PopoverContent'
|
||||
import JobAccessoryItem from '~components/JobAccessoryItem'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {
|
||||
buttonref: React.RefObject<HTMLButtonElement>
|
||||
currentAccessory?: JobAccessory
|
||||
accessories: JobAccessory[]
|
||||
editable: boolean
|
||||
open: boolean
|
||||
job: Job
|
||||
onAccessorySelected: (value: string) => void
|
||||
onOpenChange: (open: boolean) => void
|
||||
}
|
||||
|
||||
const JobAccessoryPopover = ({
|
||||
buttonref,
|
||||
currentAccessory,
|
||||
accessories,
|
||||
editable,
|
||||
open: modalOpen,
|
||||
children,
|
||||
job,
|
||||
onAccessorySelected,
|
||||
onOpenChange,
|
||||
}: PropsWithChildren<Props>) => {
|
||||
// Localization
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
// Component state
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
const classes = classNames({
|
||||
JobAccessory: true,
|
||||
ReadOnly: !editable,
|
||||
})
|
||||
|
||||
// Hooks
|
||||
useEffect(() => {
|
||||
setOpen(modalOpen)
|
||||
}, [modalOpen])
|
||||
|
||||
// Event handlers
|
||||
function handleAccessorySelected(value: string) {
|
||||
onAccessorySelected(value)
|
||||
closePopover()
|
||||
}
|
||||
|
||||
function handlePointerDownOutside(
|
||||
event: CustomEvent<{ originalEvent: PointerEvent }>
|
||||
) {
|
||||
const target = event.detail.originalEvent.target as Element
|
||||
if (
|
||||
target &&
|
||||
buttonref.current &&
|
||||
target.closest('.JobAccessory.Button') !== buttonref.current
|
||||
) {
|
||||
onOpenChange(false)
|
||||
}
|
||||
}
|
||||
|
||||
function closePopover() {
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
const radioGroup = (
|
||||
<>
|
||||
<h3>
|
||||
{capitalizeFirstLetter(
|
||||
job.accessory_type === 1
|
||||
? `${t('accessories.paladin')}s`
|
||||
: t('accessories.manadiver')
|
||||
)}
|
||||
</h3>
|
||||
<RadioGroup.Root
|
||||
className="Accessories"
|
||||
onValueChange={handleAccessorySelected}
|
||||
>
|
||||
{accessories.map((accessory) => (
|
||||
<JobAccessoryItem
|
||||
accessory={accessory}
|
||||
key={accessory.id}
|
||||
selected={
|
||||
currentAccessory && currentAccessory.id === accessory.id
|
||||
? true
|
||||
: false
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup.Root>
|
||||
</>
|
||||
)
|
||||
|
||||
const readOnly = currentAccessory ? (
|
||||
<div className="EquippedAccessory">
|
||||
<h3>
|
||||
{t('equipped')}{' '}
|
||||
{job.accessory_type === 1
|
||||
? `${t('accessories.paladin')}s`
|
||||
: t('accessories.manadiver')}
|
||||
</h3>
|
||||
<div className="Accessory">
|
||||
<img
|
||||
alt={currentAccessory.name[locale]}
|
||||
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/accessory-grid/${currentAccessory.granblue_id}.jpg`}
|
||||
/>
|
||||
<h4>{currentAccessory.name[locale]}</h4>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<h3>
|
||||
{t('no_accessory', {
|
||||
accessory: t(
|
||||
`accessories.${job.accessory_type === 1 ? 'paladin' : 'manadiver'}`
|
||||
),
|
||||
})}
|
||||
</h3>
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover open={open}>
|
||||
<PopoverTrigger asChild>{children}</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className={classes}
|
||||
onEscapeKeyDown={closePopover}
|
||||
onPointerDownOutside={handlePointerDownOutside}
|
||||
>
|
||||
{editable ? radioGroup : readOnly}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobAccessoryPopover
|
||||
|
|
@ -95,7 +95,7 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
|
|||
key={i}
|
||||
value={item.id}
|
||||
altText={item.name[locale]}
|
||||
iconSrc={`/images/job-icons/${item.granblue_id}.png`}
|
||||
iconSrc={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-icons/${item.granblue_id}.png`}
|
||||
>
|
||||
{item.name[locale]}
|
||||
</SelectItem>
|
||||
|
|
@ -116,7 +116,9 @@ const JobDropdown = React.forwardRef<HTMLSelectElement, Props>(
|
|||
value={currentJob ? currentJob.id : 'no-job'}
|
||||
altText={currentJob ? currentJob.name[locale] : ''}
|
||||
iconSrc={
|
||||
currentJob ? `/images/job-icons/${currentJob.granblue_id}.png` : ''
|
||||
currentJob
|
||||
? `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-icons/${currentJob.granblue_id}.png`
|
||||
: ''
|
||||
}
|
||||
open={open}
|
||||
onClick={openJobSelect}
|
||||
|
|
|
|||
79
components/JobImage/index.scss
Normal file
79
components/JobImage/index.scss
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
.JobImage {
|
||||
$height: 252px;
|
||||
$width: 447px;
|
||||
|
||||
aspect-ratio: 7/9;
|
||||
background: url('/images/background_a.jpg');
|
||||
background-size: 500px 281px;
|
||||
border-radius: $unit;
|
||||
box-sizing: border-box;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
isolation: isolate;
|
||||
flex-grow: 2;
|
||||
flex-shrink: 0;
|
||||
height: $height;
|
||||
margin-right: $unit * 3;
|
||||
max-height: $height;
|
||||
max-width: $width;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: $width;
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
|
||||
// prettier-ignore
|
||||
@media only screen
|
||||
and (max-width: 800px)
|
||||
and (max-height: 920px)
|
||||
and (-webkit-min-device-pixel-ratio: 2) {
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include breakpoint(phone) {
|
||||
aspect-ratio: 16/9;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
img {
|
||||
-webkit-filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.48));
|
||||
filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.48));
|
||||
position: relative;
|
||||
top: $unit * -4;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.JobAccessory.Button {
|
||||
align-items: center;
|
||||
border-radius: 99px;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
padding: $unit * 1.5;
|
||||
top: $unit;
|
||||
left: $unit;
|
||||
height: auto;
|
||||
z-index: 10;
|
||||
|
||||
&:hover .Accessory svg,
|
||||
&.Selected .Accessory svg {
|
||||
fill: var(--button-text-hover);
|
||||
}
|
||||
|
||||
.Accessory svg {
|
||||
fill: var(--button-text);
|
||||
width: $unit-3x;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.Overlay {
|
||||
background: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
114
components/JobImage/index.tsx
Normal file
114
components/JobImage/index.tsx
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
import React, { useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import Button from '~components/Button'
|
||||
import JobAccessoryPopover from '~components/JobAccessoryPopover'
|
||||
|
||||
import ShieldIcon from '~public/icons/Shield.svg'
|
||||
import ManaturaIcon from '~public/icons/Manatura.svg'
|
||||
|
||||
import './index.scss'
|
||||
import classNames from 'classnames'
|
||||
|
||||
interface Props {
|
||||
job?: Job
|
||||
currentAccessory?: JobAccessory
|
||||
accessories?: JobAccessory[]
|
||||
editable: boolean
|
||||
user?: User
|
||||
onAccessorySelected: (value: string) => void
|
||||
}
|
||||
|
||||
const JobImage = ({
|
||||
job,
|
||||
currentAccessory,
|
||||
editable,
|
||||
accessories,
|
||||
user,
|
||||
onAccessorySelected,
|
||||
}: Props) => {
|
||||
// Localization
|
||||
const router = useRouter()
|
||||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
// Component state
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
// Refs
|
||||
const buttonRef = React.createRef<HTMLButtonElement>()
|
||||
|
||||
// Static variables
|
||||
const imageUrl = () => {
|
||||
let source = ''
|
||||
|
||||
if (job) {
|
||||
const slug = job.name.en.replaceAll(' ', '-').toLowerCase()
|
||||
const gender = user && user.gender == 1 ? 'b' : 'a'
|
||||
source = `${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/jobs/${slug}_${gender}.png`
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
||||
const hasAccessory = job && job.accessory
|
||||
const image = <img alt={job?.name[locale]} src={imageUrl()} />
|
||||
|
||||
const classes = classNames({
|
||||
JobAccessory: true,
|
||||
Selected: open,
|
||||
})
|
||||
|
||||
function handleAccessoryButtonClicked() {
|
||||
setOpen(!open)
|
||||
}
|
||||
|
||||
function handlePopoverOpenChanged(open: boolean) {
|
||||
setOpen(open)
|
||||
}
|
||||
|
||||
// Elements
|
||||
const accessoryButton = () => {
|
||||
let icon
|
||||
|
||||
if (job && job.accessory_type === 1) icon = <ShieldIcon />
|
||||
else if (job && job.accessory_type === 2) icon = <ManaturaIcon />
|
||||
|
||||
return (
|
||||
<Button
|
||||
leftAccessoryIcon={icon}
|
||||
className={classes}
|
||||
onClick={handleAccessoryButtonClicked}
|
||||
ref={buttonRef}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const accessoryPopover = () => {
|
||||
return job && accessories ? (
|
||||
<JobAccessoryPopover
|
||||
buttonref={buttonRef}
|
||||
currentAccessory={currentAccessory}
|
||||
accessories={accessories}
|
||||
editable={editable}
|
||||
open={open}
|
||||
job={job}
|
||||
onAccessorySelected={onAccessorySelected}
|
||||
onOpenChange={handlePopoverOpenChanged}
|
||||
>
|
||||
{accessoryButton()}
|
||||
</JobAccessoryPopover>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="JobImage">
|
||||
{hasAccessory ? accessoryPopover() : ''}
|
||||
{job && job.id !== '-1' ? image : ''}
|
||||
<div className="Job Overlay" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default JobImage
|
||||
|
|
@ -53,63 +53,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.JobImage {
|
||||
$height: 252px;
|
||||
$width: 447px;
|
||||
|
||||
aspect-ratio: 7/9;
|
||||
background: url('/images/background_a.jpg');
|
||||
background-size: 500px 281px;
|
||||
border-radius: $unit;
|
||||
box-sizing: border-box;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2);
|
||||
display: block;
|
||||
isolation: isolate;
|
||||
flex-grow: 2;
|
||||
flex-shrink: 0;
|
||||
height: $height;
|
||||
margin-right: $unit * 3;
|
||||
max-height: $height;
|
||||
max-width: $width;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: $width;
|
||||
transition: box-shadow 0.15s ease-in-out;
|
||||
|
||||
// prettier-ignore
|
||||
@media only screen
|
||||
and (max-width: 800px)
|
||||
and (max-height: 920px)
|
||||
and (-webkit-min-device-pixel-ratio: 2) {
|
||||
margin-right: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@include breakpoint(phone) {
|
||||
aspect-ratio: 16/9;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
height: inherit;
|
||||
}
|
||||
|
||||
img {
|
||||
-webkit-filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.48));
|
||||
filter: drop-shadow(4px 4px 8px rgba(0, 0, 0, 0.48));
|
||||
position: relative;
|
||||
top: $unit * -4;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.Overlay {
|
||||
background: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.JobSkills {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ import { useSnapshot } from 'valtio'
|
|||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import JobDropdown from '~components/JobDropdown'
|
||||
import JobImage from '~components/JobImage'
|
||||
import JobSkillItem from '~components/JobSkillItem'
|
||||
import SearchModal from '~components/SearchModal'
|
||||
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import { ACCESSORY_JOB_IDS } from '~utils/jobsWithAccessories'
|
||||
import type { JobSkillObject, SearchableObject } from '~types'
|
||||
|
||||
import './index.scss'
|
||||
|
|
@ -16,9 +19,11 @@ import './index.scss'
|
|||
interface Props {
|
||||
job?: Job
|
||||
jobSkills: JobSkillObject
|
||||
jobAccessory?: JobAccessory
|
||||
editable: boolean
|
||||
saveJob: (job?: Job) => void
|
||||
saveSkill: (skill: JobSkill, position: number) => void
|
||||
saveAccessory: (accessory: JobAccessory) => void
|
||||
}
|
||||
|
||||
const JobSection = (props: Props) => {
|
||||
|
|
@ -29,13 +34,19 @@ const JobSection = (props: Props) => {
|
|||
const locale =
|
||||
router.locale && ['en', 'ja'].includes(router.locale) ? router.locale : 'en'
|
||||
|
||||
// Data state
|
||||
const [job, setJob] = useState<Job>()
|
||||
const [imageUrl, setImageUrl] = useState('')
|
||||
const [numSkills, setNumSkills] = useState(4)
|
||||
const [skills, setSkills] = useState<{ [key: number]: JobSkill | undefined }>(
|
||||
[]
|
||||
)
|
||||
const [accessories, setAccessories] = useState<JobAccessory[]>([])
|
||||
const [currentAccessory, setCurrentAccessory] = useState<
|
||||
JobAccessory | undefined
|
||||
>()
|
||||
|
||||
// Refs
|
||||
const selectRef = React.createRef<HTMLSelectElement>()
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -47,6 +58,7 @@ const JobSection = (props: Props) => {
|
|||
2: props.jobSkills[2],
|
||||
3: props.jobSkills[3],
|
||||
})
|
||||
setCurrentAccessory(props.jobAccessory)
|
||||
|
||||
if (selectRef.current && props.job) selectRef.current.value = props.job.id
|
||||
}, [props])
|
||||
|
|
@ -61,14 +73,33 @@ const JobSection = (props: Props) => {
|
|||
appState.party.job = job
|
||||
if (job.row === '1') setNumSkills(3)
|
||||
else setNumSkills(4)
|
||||
fetchJobAccessories()
|
||||
}
|
||||
}, [job])
|
||||
|
||||
// Data fetching
|
||||
async function fetchJobAccessories() {
|
||||
if (job && job.accessory) {
|
||||
const response = await api.jobAccessoriesForJob(job.id)
|
||||
const jobAccessories: JobAccessory[] = response.data
|
||||
setAccessories(jobAccessories)
|
||||
}
|
||||
}
|
||||
|
||||
function receiveJob(job?: Job) {
|
||||
setJob(job)
|
||||
props.saveJob(job)
|
||||
}
|
||||
|
||||
function handleAccessorySelected(value: string) {
|
||||
const accessory = accessories.find((accessory) => accessory.id === value)
|
||||
|
||||
if (accessory) {
|
||||
setCurrentAccessory(accessory)
|
||||
props.saveAccessory(accessory)
|
||||
}
|
||||
}
|
||||
|
||||
function generateImageUrl() {
|
||||
let imgSrc = ''
|
||||
|
||||
|
|
@ -150,14 +181,14 @@ const JobSection = (props: Props) => {
|
|||
// Render: JSX components
|
||||
return (
|
||||
<section id="Job">
|
||||
<div className="JobImage">
|
||||
{party.job && party.job.id !== '-1' ? (
|
||||
<img alt={party.job.name[locale]} src={imageUrl} />
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<div className="Job Overlay" />
|
||||
</div>
|
||||
<JobImage
|
||||
job={party.job}
|
||||
currentAccessory={currentAccessory}
|
||||
accessories={accessories}
|
||||
editable={props.editable}
|
||||
user={party.user}
|
||||
onAccessorySelected={handleAccessorySelected}
|
||||
/>
|
||||
<div className="JobDetails">
|
||||
{props.editable ? (
|
||||
<JobDropdown
|
||||
|
|
@ -166,7 +197,13 @@ const JobSection = (props: Props) => {
|
|||
ref={selectRef}
|
||||
/>
|
||||
) : (
|
||||
jobLabel()
|
||||
<div className="JobName">
|
||||
<img
|
||||
alt={party.job.name[locale]}
|
||||
src={`${process.env.NEXT_PUBLIC_SIERO_IMG_URL}/job-icons/${party.job.granblue_id}.png`}
|
||||
/>
|
||||
<h3>{party.job ? party.job.name[locale] : t('no_job')}</h3>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ul className="JobSkills">
|
||||
|
|
|
|||
15
components/Layout/index.scss
Normal file
15
components/Layout/index.scss
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
.ToastViewport {
|
||||
position: fixed;
|
||||
bottom: 0px;
|
||||
right: 0px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 340px;
|
||||
max-width: 100vw;
|
||||
z-index: 2147483647;
|
||||
padding: 25px;
|
||||
gap: 10px;
|
||||
margin: 0px;
|
||||
list-style: none;
|
||||
outline: none;
|
||||
}
|
||||
|
|
@ -1,14 +1,72 @@
|
|||
import type { ReactElement } from 'react'
|
||||
import { PropsWithChildren, useEffect, useState } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { add, format } from 'date-fns'
|
||||
import { getCookie } from 'cookies-next'
|
||||
|
||||
import { appState } from '~utils/appState'
|
||||
|
||||
import TopHeader from '~components/Header'
|
||||
import UpdateToast from '~components/UpdateToast'
|
||||
|
||||
interface Props {
|
||||
children: ReactElement
|
||||
}
|
||||
import './index.scss'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const Layout = ({ children }: PropsWithChildren<Props>) => {
|
||||
const router = useRouter()
|
||||
const [updateToastOpen, setUpdateToastOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const cookie = getToastCookie()
|
||||
const now = new Date()
|
||||
const updatedAt = new Date(appState.version.updated_at)
|
||||
const validUntil = add(updatedAt, { days: 7 })
|
||||
|
||||
if (now < validUntil && !cookie.seen) setUpdateToastOpen(true)
|
||||
}, [])
|
||||
|
||||
function getToastCookie() {
|
||||
if (appState.version.updated_at !== '') {
|
||||
const updatedAt = new Date(appState.version.updated_at)
|
||||
const cookieValues = getCookie(
|
||||
`update-${format(updatedAt, 'yyyy-MM-dd')}`
|
||||
)
|
||||
return cookieValues
|
||||
? (JSON.parse(cookieValues as string) as { seen: true })
|
||||
: { seen: false }
|
||||
} else {
|
||||
return { seen: false }
|
||||
}
|
||||
}
|
||||
|
||||
function handleToastActionClicked() {
|
||||
setUpdateToastOpen(false)
|
||||
}
|
||||
|
||||
function handleToastClosed() {
|
||||
setUpdateToastOpen(false)
|
||||
}
|
||||
|
||||
const updateToast = () => {
|
||||
const path = router.asPath.replaceAll('/', '')
|
||||
|
||||
return !['about', 'updates', 'roadmap'].includes(path) ? (
|
||||
<UpdateToast
|
||||
open={updateToastOpen}
|
||||
updateType="feature"
|
||||
onActionClicked={handleToastActionClicked}
|
||||
onCloseClicked={handleToastClosed}
|
||||
lastUpdated={appState.version.updated_at}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
const Layout = ({ children }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<TopHeader />
|
||||
{updateToast()}
|
||||
<main>{children}</main>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { setCookie } from 'cookies-next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
|
@ -26,7 +26,12 @@ interface ErrorMap {
|
|||
const emailRegex =
|
||||
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
|
||||
const LoginModal = () => {
|
||||
interface Props {
|
||||
open: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
const LoginModal = (props: Props) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
|
|
@ -46,6 +51,10 @@ const LoginModal = () => {
|
|||
const footerRef: React.RefObject<HTMLDivElement> = React.createRef()
|
||||
const form: React.RefObject<HTMLInputElement>[] = [emailInput, passwordInput]
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(props.open)
|
||||
}, [props.open])
|
||||
|
||||
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const { name, value } = event.target
|
||||
let newErrors = { ...errors }
|
||||
|
|
@ -133,7 +142,9 @@ const LoginModal = () => {
|
|||
token: resp.access_token,
|
||||
}
|
||||
|
||||
setCookie('account', cookieObj, { path: '/' })
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 60)
|
||||
setCookie('account', cookieObj, { path: '/', expires: expiresAt })
|
||||
|
||||
// Set Axios default headers
|
||||
setUserToken()
|
||||
|
|
@ -144,24 +155,32 @@ const LoginModal = () => {
|
|||
const user = response.data
|
||||
|
||||
// Set user data in the user cookie
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 60)
|
||||
|
||||
setCookie(
|
||||
'user',
|
||||
{
|
||||
picture: user.avatar.picture,
|
||||
element: user.avatar.element,
|
||||
avatar: {
|
||||
picture: user.avatar.picture,
|
||||
element: user.avatar.element,
|
||||
},
|
||||
language: user.language,
|
||||
gender: user.gender,
|
||||
theme: user.theme,
|
||||
},
|
||||
{ path: '/' }
|
||||
{ path: '/', expires: expiresAt }
|
||||
)
|
||||
|
||||
// Set the user data in the account state
|
||||
accountState.account.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
picture: user.avatar.picture,
|
||||
element: user.avatar.element,
|
||||
granblueId: '',
|
||||
avatar: {
|
||||
picture: user.avatar.picture,
|
||||
element: user.avatar.element,
|
||||
},
|
||||
gender: user.gender,
|
||||
language: user.language,
|
||||
theme: user.theme,
|
||||
|
|
@ -180,6 +199,8 @@ const LoginModal = () => {
|
|||
email: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
if (props.onOpenChange) props.onOpenChange(open)
|
||||
}
|
||||
|
||||
function onEscapeKeyDown(event: KeyboardEvent) {
|
||||
|
|
@ -193,11 +214,6 @@ const LoginModal = () => {
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={openChange}>
|
||||
<DialogTrigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t('menu.login')}</span>
|
||||
</li>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="Login"
|
||||
footerref={footerRef}
|
||||
|
|
|
|||
|
|
@ -147,11 +147,16 @@ const Party = (props: Props) => {
|
|||
appState.party.updated_at = team.updated_at
|
||||
appState.party.job = team.job
|
||||
appState.party.jobSkills = team.job_skills
|
||||
appState.party.accessory = team.accessory
|
||||
|
||||
appState.party.id = team.id
|
||||
appState.party.shortcode = team.shortcode
|
||||
appState.party.extra = team.extra
|
||||
appState.party.user = team.user
|
||||
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.updated_at = team.updated_at
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
flex-direction: column;
|
||||
margin: $unit-4x auto 0 auto;
|
||||
max-width: $grid-width;
|
||||
padding-bottom: $unit-12x;
|
||||
|
||||
@include breakpoint(phone) {
|
||||
padding: 0 $unit;
|
||||
|
|
@ -10,13 +11,13 @@
|
|||
|
||||
.PartyDetails {
|
||||
display: none;
|
||||
margin: 0 auto;
|
||||
margin: 0 auto $unit-2x;
|
||||
max-width: $unit * 94;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
&.Visible {
|
||||
margin-bottom: $unit-12x;
|
||||
// margin-bottom: $unit-12x;
|
||||
}
|
||||
|
||||
&.Editable {
|
||||
|
|
@ -269,15 +270,21 @@
|
|||
.Left {
|
||||
flex-grow: 1;
|
||||
|
||||
h1 {
|
||||
font-size: $font-xlarge;
|
||||
font-weight: $normal;
|
||||
text-align: left;
|
||||
.Header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $unit;
|
||||
margin-bottom: $unit;
|
||||
color: var(--text-primary);
|
||||
|
||||
&.empty {
|
||||
color: var(--text-secondary);
|
||||
h1 {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import Link from 'next/link'
|
|||
import { useRouter } from 'next/router'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
|
||||
import Linkify from 'react-linkify'
|
||||
import LiteYouTubeEmbed from 'react-lite-youtube-embed'
|
||||
|
|
@ -10,17 +11,19 @@ import classNames from 'classnames'
|
|||
import reactStringReplace from 'react-string-replace'
|
||||
|
||||
import Alert from '~components/Alert'
|
||||
|
||||
import Button from '~components/Button'
|
||||
import CharLimitedFieldset from '~components/CharLimitedFieldset'
|
||||
import Input from '~components/Input'
|
||||
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 RaidDropdown from '~components/RaidDropdown'
|
||||
import TextFieldset from '~components/TextFieldset'
|
||||
import Switch from '~components/Switch'
|
||||
|
||||
import api from '~utils/api'
|
||||
import { accountState } from '~utils/accountState'
|
||||
import { appState } from '~utils/appState'
|
||||
import { formatTimeAgo } from '~utils/timeAgo'
|
||||
|
|
@ -29,6 +32,7 @@ import { youtube } from '~utils/youtube'
|
|||
import CheckIcon from '~public/icons/Check.svg'
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import EditIcon from '~public/icons/Edit.svg'
|
||||
import RemixIcon from '~public/icons/Remix.svg'
|
||||
|
||||
import type { DetailsObject } from 'types'
|
||||
|
||||
|
|
@ -69,6 +73,8 @@ const PartyDetails = (props: Props) => {
|
|||
const [turnCount, setTurnCount] = useState<number | undefined>(undefined)
|
||||
const [clearTime, setClearTime] = useState(0)
|
||||
|
||||
const [remixes, setRemixes] = useState<Party[]>([])
|
||||
|
||||
const [raidSlug, setRaidSlug] = useState('')
|
||||
const [embeddedDescription, setEmbeddedDescription] =
|
||||
useState<React.ReactNode>()
|
||||
|
|
@ -111,6 +117,7 @@ const PartyDetails = (props: Props) => {
|
|||
setFullAuto(props.party.full_auto)
|
||||
setChargeAttack(props.party.charge_attack)
|
||||
setClearTime(props.party.clear_time)
|
||||
setRemixes(props.party.remixes)
|
||||
if (props.party.turn_count) setTurnCount(props.party.turn_count)
|
||||
if (props.party.button_count) setButtonCount(props.party.button_count)
|
||||
if (props.party.chain_count) setChainCount(props.party.chain_count)
|
||||
|
|
@ -300,6 +307,49 @@ const PartyDetails = (props: Props) => {
|
|||
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) {
|
||||
// Initialize an array to store the video IDs
|
||||
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 = () => {
|
||||
if (party.editable) {
|
||||
return (
|
||||
|
|
@ -538,7 +610,7 @@ const PartyDetails = (props: Props) => {
|
|||
<div className="left">
|
||||
{router.pathname !== '/new' ? (
|
||||
<Button
|
||||
accessoryIcon={<CrossIcon />}
|
||||
leftAccessoryIcon={<CrossIcon />}
|
||||
className="Blended medium destructive"
|
||||
onClick={handleClick}
|
||||
text={t('buttons.delete')}
|
||||
|
|
@ -550,7 +622,7 @@ const PartyDetails = (props: Props) => {
|
|||
<div className="right">
|
||||
<Button text={t('buttons.cancel')} onClick={toggleDetails} />
|
||||
<Button
|
||||
accessoryIcon={<CheckIcon className="Check" />}
|
||||
leftAccessoryIcon={<CheckIcon className="Check" />}
|
||||
text={t('buttons.save_info')}
|
||||
onClick={updateDetails}
|
||||
/>
|
||||
|
|
@ -620,11 +692,35 @@ const PartyDetails = (props: Props) => {
|
|||
</section>
|
||||
)
|
||||
|
||||
const remixSection = () => {
|
||||
return (
|
||||
<section className="Remixes">
|
||||
<h3>{t('remixes')}</h3>
|
||||
{<GridRepCollection>{renderRemixes()}</GridRepCollection>}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="DetailsWrapper">
|
||||
<div className="PartyInfo">
|
||||
<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">
|
||||
{renderUserBlock()}
|
||||
{party.raid ? linkedRaidBlock(party.raid) : ''}
|
||||
|
|
@ -643,7 +739,7 @@ const PartyDetails = (props: Props) => {
|
|||
<div className="Right">
|
||||
{party.editable ? (
|
||||
<Button
|
||||
accessoryIcon={<EditIcon />}
|
||||
leftAccessoryIcon={<EditIcon />}
|
||||
text={t('buttons.show_info')}
|
||||
onClick={toggleDetails}
|
||||
/>
|
||||
|
|
@ -654,6 +750,7 @@ const PartyDetails = (props: Props) => {
|
|||
</div>
|
||||
{readOnly}
|
||||
{editable}
|
||||
{remixes && remixes.length > 0 ? remixSection() : ''}
|
||||
{deleteAlert()}
|
||||
</section>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
border-radius: $card-corner;
|
||||
border: 0.5px solid rgba(0, 0, 0, 0.18);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.24);
|
||||
transform-origin: var(--radix-popover-content-transform-origin);
|
||||
outline: none;
|
||||
padding: $unit;
|
||||
transform-origin: var(--radix-popover-content-transform-origin);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ import './index.scss'
|
|||
|
||||
interface Props
|
||||
extends React.DetailedHTMLProps<
|
||||
React.DialogHTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> {}
|
||||
React.DialogHTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>,
|
||||
PopoverPrimitive.PopoverContentProps {}
|
||||
|
||||
export const Popover = PopoverPrimitive.Root
|
||||
export const PopoverAnchor = PopoverPrimitive.Anchor
|
||||
|
|
@ -26,15 +27,18 @@ export const PopoverContent = React.forwardRef<HTMLDivElement, Props>(
|
|||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
sideOffset={5}
|
||||
{...props}
|
||||
className={classes}
|
||||
ref={forwardedRef}
|
||||
>
|
||||
{children}
|
||||
<PopoverPrimitive.Arrow />
|
||||
<PopoverPrimitive.Arrow className="Arrow" />
|
||||
</PopoverPrimitive.Content>
|
||||
</PopoverPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
PopoverContent.defaultProps = {
|
||||
sideOffset: 8,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,118 +0,0 @@
|
|||
.Roadmap.DialogContent {
|
||||
gap: 0;
|
||||
padding-bottom: $unit-2x;
|
||||
|
||||
h3.priority {
|
||||
font-weight: $medium;
|
||||
font-size: $font-large;
|
||||
margin-bottom: $unit-4x;
|
||||
|
||||
&.in_progress {
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
&.high {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
&.mid {
|
||||
color: $orange-10;
|
||||
}
|
||||
|
||||
&.low {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
padding: 0 $unit-4x;
|
||||
|
||||
section.notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
margin-bottom: $unit-2x;
|
||||
|
||||
p {
|
||||
margin-bottom: $unit;
|
||||
}
|
||||
|
||||
.LinkItem {
|
||||
$diameter: $unit-6x;
|
||||
border: 1px solid var(--link-item-bg);
|
||||
border-radius: $card-corner;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--link-item-bg);
|
||||
|
||||
svg {
|
||||
fill: var(--link-item-image-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
padding: $unit-2x;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.Left {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--link-item-image-color);
|
||||
width: $diameter;
|
||||
height: auto;
|
||||
|
||||
&.ShareIcon {
|
||||
width: $unit-4x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: $bold;
|
||||
max-width: 70%;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
|
||||
font-size: $font-regular;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
ul {
|
||||
color: var(--text-primary);
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
margin-bottom: $unit-2x;
|
||||
|
||||
h4 {
|
||||
font-size: $font-medium;
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-regular;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '~components/Dialog'
|
||||
import DialogContent from '~components/DialogContent'
|
||||
|
||||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import ShareIcon from '~public/icons/Share.svg'
|
||||
import GithubIcon from '~public/icons/github.svg'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
const RoadmapModal = () => {
|
||||
const { t } = useTranslation('roadmap')
|
||||
const headerRef = React.createRef<HTMLDivElement>()
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t('modals.roadmap.title')}</span>
|
||||
</li>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="Roadmap"
|
||||
title={t('title')}
|
||||
headerref={headerRef}
|
||||
onOpenAutoFocus={(event) => event.preventDefault()}
|
||||
onEscapeKeyDown={() => {}}
|
||||
>
|
||||
<div className="DialogHeader" ref={headerRef}>
|
||||
<DialogTitle className="DialogTitle">{t('title')}</DialogTitle>
|
||||
<DialogClose className="DialogClose" asChild>
|
||||
<span>
|
||||
<CrossIcon />
|
||||
</span>
|
||||
</DialogClose>
|
||||
</div>
|
||||
|
||||
<div className="content">
|
||||
<section className="notes">
|
||||
<p>{t('blurb')}</p>
|
||||
<p>{t('link.intro')}</p>
|
||||
<div className="LinkItem">
|
||||
<Link href="https://github.com/users/jedmund/projects/1/views/3">
|
||||
<a
|
||||
href="https://github.com/users/jedmund/projects/1/views/3"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="Left">
|
||||
<GithubIcon />
|
||||
<h3>{t('link.title')}</h3>
|
||||
</div>
|
||||
<ShareIcon className="ShareIcon" />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="features">
|
||||
<h3 className="priority in_progress">{t('subtitle')}</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<h4>{t('roadmap.item1.title')}</h4>
|
||||
<p>{t('roadmap.item1.description')}</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{t('roadmap.item2.title')}</h4>
|
||||
<p>{t('roadmap.item2.description')}</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{t('roadmap.item3.title')}</h4>
|
||||
<p>{t('roadmap.item3.description')}</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{t('roadmap.item4.title')}</h4>
|
||||
<p>{t('roadmap.item4.description')}</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{t('roadmap.item5.title')}</h4>
|
||||
<p>{t('roadmap.item5.description')}</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{t('roadmap.item6.title')}</h4>
|
||||
<p>{t('roadmap.item6.description')}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default RoadmapModal
|
||||
108
components/RoadmapPage/index.scss
Normal file
108
components/RoadmapPage/index.scss
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
.Roadmap.PageContent {
|
||||
h3.priority {
|
||||
font-weight: $medium;
|
||||
font-size: $font-large;
|
||||
margin-bottom: $unit-4x;
|
||||
|
||||
&.in_progress {
|
||||
color: $yellow;
|
||||
}
|
||||
|
||||
&.high {
|
||||
color: $red;
|
||||
}
|
||||
|
||||
&.mid {
|
||||
color: $orange-10;
|
||||
}
|
||||
|
||||
&.low {
|
||||
color: $blue;
|
||||
}
|
||||
}
|
||||
|
||||
.notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
margin-bottom: $unit-2x;
|
||||
|
||||
p {
|
||||
margin-bottom: $unit;
|
||||
}
|
||||
|
||||
.LinkItem {
|
||||
$diameter: $unit-6x;
|
||||
border: 1px solid var(--link-item-bg);
|
||||
border-radius: $card-corner;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--link-item-bg);
|
||||
|
||||
svg {
|
||||
fill: var(--link-item-image-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
padding: $unit-2x;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.Left {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--link-item-image-color);
|
||||
width: $diameter;
|
||||
height: auto;
|
||||
|
||||
&.ShareIcon {
|
||||
width: $unit-4x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: $bold;
|
||||
max-width: 70%;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
|
||||
font-size: $font-regular;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
ul {
|
||||
color: var(--text-primary);
|
||||
list-style-type: none;
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit;
|
||||
margin-bottom: $unit-2x;
|
||||
|
||||
h4 {
|
||||
font-size: $font-medium;
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: $font-regular;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
components/RoadmapPage/index.tsx
Normal file
73
components/RoadmapPage/index.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import React from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import ShareIcon from '~public/icons/Share.svg'
|
||||
import GithubIcon from '~public/icons/github.svg'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const RoadmapPage: React.FC<Props> = (props: Props) => {
|
||||
const { t: common } = useTranslation('common')
|
||||
const { t: roadmap } = useTranslation('roadmap')
|
||||
return (
|
||||
<div className="Roadmap PageContent">
|
||||
<h1>{common('about.segmented_control.roadmap')}</h1>
|
||||
<section className="notes">
|
||||
<p>{roadmap('blurb')}</p>
|
||||
<p>{roadmap('link.intro')}</p>
|
||||
<div className="LinkItem">
|
||||
<Link href="https://github.com/users/jedmund/projects/1/views/3">
|
||||
<a
|
||||
href="https://github.com/users/jedmund/projects/1/views/3"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<div className="Left">
|
||||
<GithubIcon />
|
||||
<h3>{roadmap('link.title')}</h3>
|
||||
</div>
|
||||
<ShareIcon className="ShareIcon" />
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="features">
|
||||
<h3 className="priority in_progress">{roadmap('subtitle')}</h3>
|
||||
<ul>
|
||||
<li>
|
||||
<h4>{roadmap('roadmap.item1.title')}</h4>
|
||||
<p>{roadmap('roadmap.item1.description')}</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{roadmap('roadmap.item2.title')}</h4>
|
||||
<p>{roadmap('roadmap.item2.description')}</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{roadmap('roadmap.item3.title')}</h4>
|
||||
<p>{roadmap('roadmap.item3.description')}</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{roadmap('roadmap.item4.title')}</h4>
|
||||
<p>{roadmap('roadmap.item4.description')}</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{roadmap('roadmap.item5.title')}</h4>
|
||||
<p>{roadmap('roadmap.item5.description')}</p>
|
||||
</li>
|
||||
<li>
|
||||
<h4>{roadmap('roadmap.item6.title')}</h4>
|
||||
<p>{roadmap('roadmap.item6.description')}</p>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RoadmapPage
|
||||
|
|
@ -142,8 +142,14 @@ const SearchModal = (props: Props) => {
|
|||
}
|
||||
}
|
||||
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 60)
|
||||
|
||||
if (recents && recents.length > 5) recents.pop()
|
||||
setCookie(`recent_${props.object}`, recents, { path: '/' })
|
||||
setCookie(`recent_${props.object}`, recents, {
|
||||
path: '/',
|
||||
expires: expiresAt,
|
||||
})
|
||||
sendData(result)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ const SelectItem = React.forwardRef<HTMLDivElement, Props>(function selectItem(
|
|||
{ children, value, iconSrc, altText, ...props },
|
||||
forwardedRef
|
||||
) {
|
||||
const { altText, iconSrc, ...rest } = props
|
||||
return (
|
||||
<Select.Item
|
||||
className={classNames('SelectItem', props.className)}
|
||||
{...props}
|
||||
{...rest}
|
||||
ref={forwardedRef}
|
||||
value={`${value}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { setCookie } from 'cookies-next'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
|
@ -15,7 +15,10 @@ import DialogContent from '~components/DialogContent'
|
|||
import CrossIcon from '~public/icons/Cross.svg'
|
||||
import './index.scss'
|
||||
|
||||
interface Props {}
|
||||
interface Props {
|
||||
open: boolean
|
||||
onOpenChange?: (open: boolean) => void
|
||||
}
|
||||
|
||||
interface ErrorMap {
|
||||
[index: string]: string
|
||||
|
|
@ -58,6 +61,10 @@ const SignupModal = (props: Props) => {
|
|||
passwordConfirmationInput,
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(props.open)
|
||||
}, [props.open])
|
||||
|
||||
function register(event: React.FormEvent) {
|
||||
event.preventDefault()
|
||||
|
||||
|
|
@ -91,7 +98,9 @@ const SignupModal = (props: Props) => {
|
|||
token: resp.token,
|
||||
}
|
||||
|
||||
setCookie('account', cookieObj, { path: '/' })
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 60)
|
||||
setCookie('account', cookieObj, { path: '/', expires: expiresAt })
|
||||
|
||||
// Set Axios default headers
|
||||
setUserToken()
|
||||
|
|
@ -106,24 +115,32 @@ const SignupModal = (props: Props) => {
|
|||
const user = response.data
|
||||
|
||||
// Set user data in the user cookie
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 60)
|
||||
|
||||
setCookie(
|
||||
'user',
|
||||
{
|
||||
picture: user.avatar.picture,
|
||||
element: user.avatar.element,
|
||||
avatar: {
|
||||
picture: user.avatar.picture,
|
||||
element: user.avatar.element,
|
||||
},
|
||||
language: user.language,
|
||||
gender: user.gender,
|
||||
theme: user.theme,
|
||||
},
|
||||
{ path: '/' }
|
||||
{ path: '/', expires: expiresAt }
|
||||
)
|
||||
|
||||
// Set the user data in the account state
|
||||
accountState.account.user = {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
picture: user.avatar.picture,
|
||||
element: user.avatar.element,
|
||||
granblueId: '',
|
||||
avatar: {
|
||||
picture: user.avatar.picture,
|
||||
element: user.avatar.element,
|
||||
},
|
||||
gender: user.gender,
|
||||
language: user.language,
|
||||
theme: user.theme,
|
||||
|
|
@ -261,6 +278,8 @@ const SignupModal = (props: Props) => {
|
|||
password: '',
|
||||
passwordConfirmation: '',
|
||||
})
|
||||
|
||||
if (props.onOpenChange) props.onOpenChange(open)
|
||||
}
|
||||
|
||||
function onEscapeKeyDown(event: KeyboardEvent) {
|
||||
|
|
@ -274,11 +293,6 @@ const SignupModal = (props: Props) => {
|
|||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={openChange}>
|
||||
<DialogTrigger asChild>
|
||||
<li className="MenuItem">
|
||||
<span>{t('menu.signup')}</span>
|
||||
</li>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="Signup"
|
||||
footerref={footerRef}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ import { getCookie } from 'cookies-next'
|
|||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { AxiosError, AxiosResponse } from 'axios'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import Alert from '~components/Alert'
|
||||
import SummonUnit from '~components/SummonUnit'
|
||||
import ExtraSummons from '~components/ExtraSummons'
|
||||
|
||||
|
|
@ -38,6 +39,10 @@ const SummonGrid = (props: Props) => {
|
|||
// Localization
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Set up state for error handling
|
||||
const [axiosError, setAxiosError] = useState<AxiosResponse>()
|
||||
const [errorAlertOpen, setErrorAlertOpen] = useState(false)
|
||||
|
||||
// Set up state for view management
|
||||
const { party, grid } = useSnapshot(appState)
|
||||
const [slug, setSlug] = useState()
|
||||
|
|
@ -100,12 +105,12 @@ const SummonGrid = (props: Props) => {
|
|||
saveSummon(party.id, summon, position)
|
||||
.then((response) => handleSummonResponse(response.data))
|
||||
.catch((error) => {
|
||||
const code = error.response.status
|
||||
const data = error.response.data
|
||||
if (code === 422) {
|
||||
if (data.code === 'incompatible_summon_for_position') {
|
||||
// setShowIncompatibleAlert(true)
|
||||
}
|
||||
const axiosError = error as AxiosError
|
||||
const response = axiosError.response
|
||||
|
||||
if (response) {
|
||||
setErrorAlertOpen(true)
|
||||
setAxiosError(response)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -380,6 +385,18 @@ const SummonGrid = (props: Props) => {
|
|||
}
|
||||
|
||||
// Render: JSX components
|
||||
const errorAlert = () => {
|
||||
return (
|
||||
<Alert
|
||||
open={errorAlertOpen}
|
||||
title={axiosError ? `${axiosError.status}` : 'Error'}
|
||||
message={t(`errors.${axiosError?.statusText.toLowerCase()}`)}
|
||||
cancelAction={() => setErrorAlertOpen(false)}
|
||||
cancelActionText={t('buttons.confirm')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const mainSummonElement = (
|
||||
<div className="LabeledUnit">
|
||||
<div className="Label">{t('summons.main')}</div>
|
||||
|
|
@ -460,6 +477,7 @@ const SummonGrid = (props: Props) => {
|
|||
</div>
|
||||
|
||||
{subAuraSummonElement}
|
||||
{errorAlert()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -113,6 +113,7 @@ const SummonUnit = ({
|
|||
|
||||
function removeSummon() {
|
||||
if (gridSummon) sendSummonToRemove(gridSummon.id)
|
||||
setAlertOpen(false)
|
||||
}
|
||||
|
||||
// Methods: Image string generation
|
||||
|
|
@ -172,7 +173,7 @@ const SummonUnit = ({
|
|||
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<Button
|
||||
accessoryIcon={<SettingsIcon />}
|
||||
leftAccessoryIcon={<SettingsIcon />}
|
||||
className={buttonClasses}
|
||||
onClick={handleButtonClicked}
|
||||
/>
|
||||
|
|
|
|||
56
components/Toast/index.scss
Normal file
56
components/Toast/index.scss
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
.Toast {
|
||||
background: var(--dialog-bg);
|
||||
border-radius: $card-corner;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12), 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-2x;
|
||||
padding: $unit-3x;
|
||||
|
||||
&[data-state='open'] {
|
||||
animation: slideLeft 150ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
&[data-state='closed'] {
|
||||
animation: fadeOut 300ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
&[data-swipe='move'] {
|
||||
transform: translateX(var(--radix-toast-swipe-move-x));
|
||||
}
|
||||
|
||||
&[data-swipe='cancel'] {
|
||||
transform: translateX(0);
|
||||
transition: transform 200ms ease-out;
|
||||
}
|
||||
|
||||
&[data-swipe='end'] {
|
||||
animation: slideRight 100ms ease-out;
|
||||
}
|
||||
|
||||
.Header {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
h3 {
|
||||
font-size: $font-regular;
|
||||
font-weight: $medium;
|
||||
}
|
||||
|
||||
button {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: $font-regular;
|
||||
font-weight: $bold;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
50
components/Toast/index.tsx
Normal file
50
components/Toast/index.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import React, { PropsWithChildren } from 'react'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import * as ToastPrimitive from '@radix-ui/react-toast'
|
||||
import './index.scss'
|
||||
|
||||
interface Props extends ToastPrimitive.ToastProps {
|
||||
className?: string
|
||||
title?: string
|
||||
content: string
|
||||
onCloseClick: () => void
|
||||
}
|
||||
|
||||
const Toast = ({
|
||||
children,
|
||||
title,
|
||||
content,
|
||||
...props
|
||||
}: PropsWithChildren<Props>) => {
|
||||
const { onCloseClick, ...toastProps } = props
|
||||
|
||||
const classes = classNames(props.className, {
|
||||
Toast: true,
|
||||
})
|
||||
|
||||
return (
|
||||
<ToastPrimitive.Root {...toastProps} className={classes}>
|
||||
{title && (
|
||||
<div className="Header">
|
||||
<ToastPrimitive.Title asChild>
|
||||
<h3>{title}</h3>
|
||||
</ToastPrimitive.Title>
|
||||
<ToastPrimitive.Close aria-label="Close" onClick={onCloseClick}>
|
||||
<span aria-hidden>×</span>
|
||||
</ToastPrimitive.Close>
|
||||
</div>
|
||||
)}
|
||||
<ToastPrimitive.Description asChild>
|
||||
<p>{content}</p>
|
||||
</ToastPrimitive.Description>
|
||||
{children && (
|
||||
<ToastPrimitive.Action asChild altText={content}>
|
||||
{children}
|
||||
</ToastPrimitive.Action>
|
||||
)}
|
||||
</ToastPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default Toast
|
||||
8
components/Tooltip/index.scss
Normal file
8
components/Tooltip/index.scss
Normal 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;
|
||||
}
|
||||
39
components/Tooltip/index.tsx
Normal file
39
components/Tooltip/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@
|
|||
display: flex;
|
||||
width: $unit-10x;
|
||||
height: $unit-10x;
|
||||
padding: $unit;
|
||||
justify-content: center;
|
||||
z-index: 32;
|
||||
|
||||
|
|
|
|||
11
components/UpdateToast/index.scss
Normal file
11
components/UpdateToast/index.scss
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.Notice {
|
||||
align-items: center;
|
||||
border-radius: $card-corner;
|
||||
background: blue;
|
||||
display: flex;
|
||||
padding: $unit;
|
||||
|
||||
p {
|
||||
font-size: $font-small;
|
||||
}
|
||||
}
|
||||
74
components/UpdateToast/index.tsx
Normal file
74
components/UpdateToast/index.tsx
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import React from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { setCookie } from 'cookies-next'
|
||||
import { add, format } from 'date-fns'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import Button from '~components/Button'
|
||||
import Toast from '~components/Toast'
|
||||
|
||||
import './index.scss'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
updateType: 'feature' | 'content'
|
||||
lastUpdated: string
|
||||
onActionClicked: () => void
|
||||
onCloseClicked: () => void
|
||||
}
|
||||
|
||||
const UpdateToast = ({
|
||||
open,
|
||||
updateType,
|
||||
lastUpdated,
|
||||
onActionClicked,
|
||||
onCloseClicked,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation('roadmap')
|
||||
|
||||
const classes = classNames({
|
||||
Update: true,
|
||||
})
|
||||
|
||||
function setToastCookie() {
|
||||
const updatedAt = new Date(lastUpdated)
|
||||
const expiresAt = add(updatedAt, { days: 7 })
|
||||
setCookie(
|
||||
`update-${format(updatedAt, 'yyyy-MM-dd')}`,
|
||||
{ seen: true },
|
||||
{ path: '/', expires: expiresAt }
|
||||
)
|
||||
}
|
||||
|
||||
function handleButtonClicked() {
|
||||
window.open('/updates', '_blank')
|
||||
setToastCookie()
|
||||
onActionClicked()
|
||||
}
|
||||
|
||||
function handleCloseClicked() {
|
||||
setToastCookie()
|
||||
onCloseClicked()
|
||||
}
|
||||
|
||||
return (
|
||||
<Toast
|
||||
className={classes}
|
||||
title={t(`toasts.title`)}
|
||||
content={t(`toasts.description.${updateType}`)}
|
||||
open={open}
|
||||
type="background"
|
||||
onCloseClick={handleCloseClicked}
|
||||
>
|
||||
<Button
|
||||
buttonSize="small"
|
||||
contained={true}
|
||||
onClick={handleButtonClicked}
|
||||
text={t('toasts.button')}
|
||||
/>
|
||||
</Toast>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdateToast
|
||||
|
|
@ -1,17 +1,4 @@
|
|||
.Changelog.DialogContent {
|
||||
gap: 0;
|
||||
|
||||
.updates {
|
||||
padding: 0 $unit-4x;
|
||||
}
|
||||
|
||||
.updates {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-4x;
|
||||
margin-bottom: $unit-4x;
|
||||
}
|
||||
|
||||
.Updates.PageContent {
|
||||
.version {
|
||||
&.content {
|
||||
.top h3 {
|
||||
122
components/UpdatesPage/index.tsx
Normal file
122
components/UpdatesPage/index.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import React from 'react'
|
||||
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import ChangelogUnit from '~components/ChangelogUnit'
|
||||
|
||||
import './index.scss'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const UpdatesPage: React.FC<Props> = (props: Props) => {
|
||||
const { t: common } = useTranslation('common')
|
||||
return (
|
||||
<div className="Updates PageContent">
|
||||
<h1>{common('about.segmented_control.updates')}</h1>
|
||||
<section className="version" data-version="1.0">
|
||||
<div className="top">
|
||||
<h3>1.0.1</h3>
|
||||
<time>2023/01/08</time>
|
||||
</div>
|
||||
<ul className="notes">
|
||||
<li>Extra party fields: Full Auto, Clear Time, and more</li>
|
||||
<li>Support for Youtube short URLs</li>
|
||||
<li>Responsive grids and lots of other mobile fixes</li>
|
||||
<li>Many other bug fixes</li>
|
||||
</ul>
|
||||
</section>
|
||||
<section className="content version" data-version="2022-12L">
|
||||
<div className="top">
|
||||
<h3>2022-12 Legend Festival</h3>
|
||||
<time>2022/12/26</time>
|
||||
</div>
|
||||
<div className="update">
|
||||
<section className="characters">
|
||||
<h4>New characters</h4>
|
||||
<div className="items">
|
||||
<ChangelogUnit
|
||||
name="Michael (Grand)"
|
||||
id="3040440000"
|
||||
type="character"
|
||||
/>
|
||||
<ChangelogUnit name="Makura" id="3040441000" type="character" />
|
||||
<ChangelogUnit
|
||||
name="Ultimate Friday"
|
||||
id="3040442000"
|
||||
type="character"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section className="weapons">
|
||||
<h4>New weapons</h4>
|
||||
<div className="items">
|
||||
<ChangelogUnit
|
||||
name="Crimson Scale"
|
||||
id="1040315900"
|
||||
type="weapon"
|
||||
/>
|
||||
<ChangelogUnit name="Leporidius" id="1040914500" type="weapon" />
|
||||
<ChangelogUnit name="FRIED Spear" id="1040218200" type="weapon" />
|
||||
</div>
|
||||
</section>
|
||||
<section className="summons">
|
||||
<h4>New summons</h4>
|
||||
<div className="items">
|
||||
<ChangelogUnit name="Yatima" id="2040417000" type="summon" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<section className="content version" data-version="2022-12F2">
|
||||
<div className="top">
|
||||
<h3>2022-12 Flash Gala</h3>
|
||||
<time>2022/12/26</time>
|
||||
</div>
|
||||
<div className="update">
|
||||
<section className="characters">
|
||||
<h4>New characters</h4>
|
||||
<div className="items">
|
||||
<ChangelogUnit
|
||||
name="Charlotta (Grand)"
|
||||
id="3040438000"
|
||||
type="character"
|
||||
/>
|
||||
<ChangelogUnit name="Erin" id="3040439000" type="character" />
|
||||
</div>
|
||||
</section>
|
||||
<section className="weapons">
|
||||
<h4>New weapons</h4>
|
||||
<div className="items">
|
||||
<ChangelogUnit
|
||||
name="Claíomh Solais Díon"
|
||||
id="1040024200"
|
||||
type="weapon"
|
||||
/>
|
||||
<ChangelogUnit
|
||||
name="Crystal Edge"
|
||||
id="1040116500"
|
||||
type="weapon"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<section className="version" data-version="1.0">
|
||||
<div className="top">
|
||||
<h3>1.0.0</h3>
|
||||
<time>2022/12/26</time>
|
||||
</div>
|
||||
<ul className="notes">
|
||||
<li>First release!</li>
|
||||
<li>You can embed Youtube videos now</li>
|
||||
<li>Better clicking - right-click and open in a new tab</li>
|
||||
<li>Manually set dark mode in Account Settings</li>
|
||||
<li>Lots of bugs squashed</li>
|
||||
</ul>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default UpdatesPage
|
||||
|
|
@ -4,7 +4,7 @@ import { getCookie } from 'cookies-next'
|
|||
import { useSnapshot } from 'valtio'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
|
||||
import { AxiosResponse } from 'axios'
|
||||
import { AxiosError, AxiosResponse } from 'axios'
|
||||
import debounce from 'lodash.debounce'
|
||||
|
||||
import Alert from '~components/Alert'
|
||||
|
|
@ -41,11 +41,15 @@ const WeaponGrid = (props: Props) => {
|
|||
? JSON.parse(cookie as string)
|
||||
: null
|
||||
|
||||
// Set up state for error handling
|
||||
const [axiosError, setAxiosError] = useState<AxiosResponse>()
|
||||
const [errorAlertOpen, setErrorAlertOpen] = useState(false)
|
||||
const [showIncompatibleAlert, setShowIncompatibleAlert] = useState(false)
|
||||
|
||||
// Set up state for view management
|
||||
const { party, grid } = useSnapshot(appState)
|
||||
const [slug, setSlug] = useState()
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [showIncompatibleAlert, setShowIncompatibleAlert] = useState(false)
|
||||
|
||||
// Set up state for conflict management
|
||||
const [incoming, setIncoming] = useState<Weapon>()
|
||||
|
|
@ -100,11 +104,21 @@ const WeaponGrid = (props: Props) => {
|
|||
saveWeapon(party.id, weapon, position)
|
||||
.then((response) => handleWeaponResponse(response.data))
|
||||
.catch((error) => {
|
||||
const code = error.response.status
|
||||
const data = error.response.data
|
||||
if (code === 422) {
|
||||
if (data.code === 'incompatible_weapon_for_position') {
|
||||
const axiosError = error as AxiosError
|
||||
const response = axiosError.response
|
||||
|
||||
if (response) {
|
||||
const code = response.status
|
||||
const data = response.data
|
||||
|
||||
if (
|
||||
code === 422 &&
|
||||
data.code === 'incompatible_weapon_for_position'
|
||||
) {
|
||||
setShowIncompatibleAlert(true)
|
||||
} else {
|
||||
setErrorAlertOpen(true)
|
||||
setAxiosError(response)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -362,16 +376,29 @@ const WeaponGrid = (props: Props) => {
|
|||
cancelAction={() => setShowIncompatibleAlert(!showIncompatibleAlert)}
|
||||
cancelActionText={t('buttons.confirm')}
|
||||
message={t('alert.incompatible_weapon')}
|
||||
></Alert>
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
|
||||
const errorAlert = () => {
|
||||
return (
|
||||
<Alert
|
||||
open={errorAlertOpen}
|
||||
title={axiosError ? `${axiosError.status}` : 'Error'}
|
||||
message={t(`errors.${axiosError?.statusText.toLowerCase()}`)}
|
||||
cancelAction={() => setErrorAlertOpen(false)}
|
||||
cancelActionText={t('buttons.confirm')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="WeaponGrid">
|
||||
{conflicts ? conflictModal() : ''}
|
||||
{incompatibleAlert()}
|
||||
{errorAlert()}
|
||||
<div id="MainGrid">
|
||||
{mainhandElement}
|
||||
<ul id="Weapons">{weaponGridElement}</ul>
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ const WeaponUnit = ({
|
|||
|
||||
function removeWeapon() {
|
||||
if (gridWeapon) sendWeaponToRemove(gridWeapon.id)
|
||||
setAlertOpen(false)
|
||||
}
|
||||
|
||||
// Methods: Data fetching and manipulation
|
||||
|
|
@ -450,7 +451,7 @@ const WeaponUnit = ({
|
|||
<ContextMenu onOpenChange={handleContextMenuOpenChange}>
|
||||
<ContextMenuTrigger asChild>
|
||||
<Button
|
||||
accessoryIcon={<SettingsIcon />}
|
||||
leftAccessoryIcon={<SettingsIcon />}
|
||||
className={buttonClasses}
|
||||
onClick={handleButtonClicked}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -15,6 +15,26 @@ module.exports = {
|
|||
source: '/',
|
||||
destination: '/new',
|
||||
},
|
||||
{
|
||||
source: '/characters',
|
||||
destination: '/new',
|
||||
},
|
||||
{
|
||||
source: '/summons',
|
||||
destination: '/new',
|
||||
},
|
||||
{
|
||||
source: '/weapons',
|
||||
destination: '/new',
|
||||
},
|
||||
{
|
||||
source: '/updates',
|
||||
destination: '/about',
|
||||
},
|
||||
{
|
||||
source: '/roadmap',
|
||||
destination: '/about',
|
||||
},
|
||||
{
|
||||
source: '/p/:shortcode/characters',
|
||||
destination: '/p/:shortcode',
|
||||
|
|
|
|||
186
package-lock.json
generated
186
package-lock.json
generated
|
|
@ -11,14 +11,17 @@
|
|||
"@radix-ui/react-dropdown-menu": "^2.0.1",
|
||||
"@radix-ui/react-hover-card": "^1.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^1.1.1",
|
||||
"@radix-ui/react-select": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.0.1",
|
||||
"@radix-ui/react-toast": "^1.1.2",
|
||||
"@radix-ui/react-toggle-group": "^1.0.1",
|
||||
"@radix-ui/react-tooltip": "^1.0.3",
|
||||
"@svgr/webpack": "^6.2.0",
|
||||
"axios": "^0.25.0",
|
||||
"classnames": "^2.3.1",
|
||||
"cookies-next": "^2.1.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"fix-date": "^1.1.6",
|
||||
"i18next": "^21.6.13",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
|
|
@ -2436,6 +2439,49 @@
|
|||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-radio-group": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.1.tgz",
|
||||
"integrity": "sha512-fmg1CuDKt3GAkL3YnHekmdOicyrXlbp/s/D0MrHa+YB2Un+umpJGheiRowlQtxSpb1eeehKNTINgNESi8WK5rA==",
|
||||
"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-direction": "1.0.0",
|
||||
"@radix-ui/react-presence": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.1",
|
||||
"@radix-ui/react-roving-focus": "1.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0",
|
||||
"@radix-ui/react-use-previous": "1.0.0",
|
||||
"@radix-ui/react-use-size": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz",
|
||||
"integrity": "sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-collection": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-direction": "1.0.0",
|
||||
"@radix-ui/react-id": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.1",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz",
|
||||
|
|
@ -2578,6 +2624,52 @@
|
|||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz",
|
||||
|
|
@ -3861,6 +3953,18 @@
|
|||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA==",
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
|
|
@ -8947,6 +9051,43 @@
|
|||
"@radix-ui/react-slot": "1.0.1"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-radio-group": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.1.1.tgz",
|
||||
"integrity": "sha512-fmg1CuDKt3GAkL3YnHekmdOicyrXlbp/s/D0MrHa+YB2Un+umpJGheiRowlQtxSpb1eeehKNTINgNESi8WK5rA==",
|
||||
"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-direction": "1.0.0",
|
||||
"@radix-ui/react-presence": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.1",
|
||||
"@radix-ui/react-roving-focus": "1.0.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0",
|
||||
"@radix-ui/react-use-previous": "1.0.0",
|
||||
"@radix-ui/react-use-size": "1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-roving-focus": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.2.tgz",
|
||||
"integrity": "sha512-HLK+CqD/8pN6GfJm3U+cqpqhSKYAWiOJDe+A+8MfxBnOue39QEeMa43csUn2CXCHQT0/mewh1LrrG4tfkM9DMA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-collection": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-direction": "1.0.0",
|
||||
"@radix-ui/react-id": "1.0.0",
|
||||
"@radix-ui/react-primitive": "1.0.1",
|
||||
"@radix-ui/react-use-callback-ref": "1.0.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-roving-focus": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.1.tgz",
|
||||
|
|
@ -9062,6 +9203,46 @@
|
|||
"@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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz",
|
||||
|
|
@ -9956,6 +10137,11 @@
|
|||
"integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
|
||||
"dev": true
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.29.3",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.29.3.tgz",
|
||||
"integrity": "sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
|
|
|
|||
|
|
@ -16,14 +16,17 @@
|
|||
"@radix-ui/react-dropdown-menu": "^2.0.1",
|
||||
"@radix-ui/react-hover-card": "^1.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^1.1.1",
|
||||
"@radix-ui/react-select": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.0.1",
|
||||
"@radix-ui/react-toast": "^1.1.2",
|
||||
"@radix-ui/react-toggle-group": "^1.0.1",
|
||||
"@radix-ui/react-tooltip": "^1.0.3",
|
||||
"@svgr/webpack": "^6.2.0",
|
||||
"axios": "^0.25.0",
|
||||
"classnames": "^2.3.1",
|
||||
"cookies-next": "^2.1.1",
|
||||
"date-fns": "^2.29.3",
|
||||
"fix-date": "^1.1.6",
|
||||
"i18next": "^21.6.13",
|
||||
"i18next-browser-languagedetector": "^6.1.3",
|
||||
|
|
|
|||
|
|
@ -9,12 +9,16 @@ import InfiniteScroll from 'react-infinite-scroll-component'
|
|||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
|
||||
import api from '~utils/api'
|
||||
|
||||
import setUserToken from '~utils/setUserToken'
|
||||
import extractFilters from '~utils/extractFilters'
|
||||
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
||||
import organizeRaids from '~utils/organizeRaids'
|
||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||
import { appState } from '~utils/appState'
|
||||
import { elements, allElement } from '~data/elements'
|
||||
import { emptyPaginationObject } from '~utils/emptyStates'
|
||||
import { printError } from '~utils/reportError'
|
||||
|
||||
import GridRep from '~components/GridRep'
|
||||
import GridRepCollection from '~components/GridRepCollection'
|
||||
|
|
@ -29,6 +33,7 @@ interface Props {
|
|||
meta: PaginationObject
|
||||
raids: Raid[]
|
||||
sortedRaids: Raid[][]
|
||||
version: AppUpdate
|
||||
}
|
||||
|
||||
const ProfileRoute: React.FC<Props> = (props: Props) => {
|
||||
|
|
@ -98,6 +103,7 @@ const ProfileRoute: React.FC<Props> = (props: Props) => {
|
|||
setTotalPages(props.meta.totalPages)
|
||||
setRecordCount(props.meta.count)
|
||||
replaceResults(props.meta.count, props.teams)
|
||||
appState.version = props.version
|
||||
}
|
||||
setCurrentPage(1)
|
||||
}, [])
|
||||
|
|
@ -350,50 +356,58 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Set headers for server-side requests
|
||||
setUserToken(req, res)
|
||||
|
||||
// Fetch and organize raids
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
.getAll()
|
||||
.then((response) => organizeRaids(response.data))
|
||||
try {
|
||||
// Fetch latest version
|
||||
const version = await fetchLatestVersion()
|
||||
|
||||
// Create filter object
|
||||
const filters: FilterObject = extractFilters(query, raids)
|
||||
const params = {
|
||||
params: { ...filters },
|
||||
}
|
||||
// Fetch and organize raids
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
.getAll()
|
||||
.then((response) => organizeRaids(response.data))
|
||||
|
||||
// Set up empty variables
|
||||
let user: User | null = null
|
||||
let teams: Party[] | null = null
|
||||
let meta: PaginationObject = emptyPaginationObject
|
||||
// Create filter object
|
||||
const filters: FilterObject = extractFilters(query, raids)
|
||||
const params = {
|
||||
params: { ...filters },
|
||||
}
|
||||
|
||||
// Perform a request only if we received a username
|
||||
if (query.username) {
|
||||
const response = await api.endpoints.users.getOne({
|
||||
id: query.username,
|
||||
params,
|
||||
})
|
||||
// Set up empty variables
|
||||
let user: User | null = null
|
||||
let teams: Party[] | null = null
|
||||
let meta: PaginationObject = emptyPaginationObject
|
||||
|
||||
// Assign values to pass to props
|
||||
user = response.data.profile
|
||||
// Perform a request only if we received a username
|
||||
if (query.username) {
|
||||
const response = await api.endpoints.users.getOne({
|
||||
id: query.username,
|
||||
params,
|
||||
})
|
||||
|
||||
if (response.data.profile.parties) teams = response.data.profile.parties
|
||||
else teams = []
|
||||
// Assign values to pass to props
|
||||
user = response.data.profile
|
||||
|
||||
meta.count = response.data.meta.count
|
||||
meta.totalPages = response.data.meta.total_pages
|
||||
meta.perPage = response.data.meta.per_page
|
||||
}
|
||||
if (response.data.profile.parties) teams = response.data.profile.parties
|
||||
else teams = []
|
||||
|
||||
return {
|
||||
props: {
|
||||
user: user,
|
||||
teams: teams,
|
||||
meta: meta,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
meta.count = response.data.meta.count
|
||||
meta.totalPages = response.data.meta.total_pages
|
||||
meta.perPage = response.data.meta.per_page
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
user: user,
|
||||
teams: teams,
|
||||
meta: meta,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
version: version,
|
||||
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
printError(error, 'axios')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { getCookie, getCookies } from 'cookies-next'
|
||||
import { useEffect } from 'react'
|
||||
import { getCookie } from 'cookies-next'
|
||||
import { appWithTranslation } from 'next-i18next'
|
||||
import { ThemeProvider, useTheme } from 'next-themes'
|
||||
import { ThemeProvider } from 'next-themes'
|
||||
|
||||
import type { AppProps } from 'next/app'
|
||||
import Layout from '~components/Layout'
|
||||
|
|
@ -10,6 +10,8 @@ import { accountState } from '~utils/accountState'
|
|||
import setUserToken from '~utils/setUserToken'
|
||||
|
||||
import '../styles/globals.scss'
|
||||
import { ToastProvider, Viewport } from '@radix-ui/react-toast'
|
||||
import { TooltipProvider } from '@radix-ui/react-tooltip'
|
||||
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
const accountCookie = getCookie('account')
|
||||
|
|
@ -30,8 +32,11 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||
accountState.account.user = {
|
||||
id: cookieData.account.userId,
|
||||
username: cookieData.account.username,
|
||||
picture: cookieData.user.picture,
|
||||
element: cookieData.user.element,
|
||||
granblueId: '',
|
||||
avatar: {
|
||||
picture: cookieData.user.avatar.picture,
|
||||
element: cookieData.user.avatar.element,
|
||||
},
|
||||
gender: cookieData.user.gender,
|
||||
language: cookieData.user.language,
|
||||
theme: cookieData.user.theme,
|
||||
|
|
@ -43,9 +48,14 @@ function MyApp({ Component, pageProps }: AppProps) {
|
|||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
<ToastProvider swipeDirection="right">
|
||||
<TooltipProvider>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
<Viewport className="ToastViewport" />
|
||||
</TooltipProvider>
|
||||
</ToastProvider>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
174
pages/about.tsx
Normal file
174
pages/about.tsx
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import React, { useEffect, useState } from 'react'
|
||||
import Head from 'next/head'
|
||||
|
||||
import { useRouter } from 'next/router'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
|
||||
import { AboutTabs } from '~utils/enums'
|
||||
import setUserToken from '~utils/setUserToken'
|
||||
|
||||
import AboutPage from '~components/AboutPage'
|
||||
import UpdatesPage from '~components/UpdatesPage'
|
||||
import RoadmapPage from '~components/RoadmapPage'
|
||||
import SegmentedControl from '~components/SegmentedControl'
|
||||
import Segment from '~components/Segment'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
|
||||
interface Props {}
|
||||
|
||||
const AboutRoute: React.FC<Props> = (props: Props) => {
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
|
||||
// Import translations
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<AboutTabs>(AboutTabs.About)
|
||||
const [currentPage, setCurrentPage] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const parts = router.asPath.split('/')
|
||||
const tab = parts[parts.length - 1]
|
||||
|
||||
switch (tab) {
|
||||
case 'about':
|
||||
setCurrentTab(AboutTabs.About)
|
||||
setCurrentPage(parts[1])
|
||||
break
|
||||
case 'updates':
|
||||
setCurrentTab(AboutTabs.Updates)
|
||||
setCurrentPage(parts[1])
|
||||
break
|
||||
case 'roadmap':
|
||||
setCurrentTab(AboutTabs.Roadmap)
|
||||
setCurrentPage(parts[1])
|
||||
break
|
||||
}
|
||||
}, [router.asPath])
|
||||
|
||||
function handleTabClicked(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
const parts = router.asPath.split('/')
|
||||
const path = `/${event.target.value}`
|
||||
|
||||
switch (event.target.value) {
|
||||
case 'about':
|
||||
router.replace(path)
|
||||
setCurrentTab(AboutTabs.About)
|
||||
setCurrentPage(parts[1])
|
||||
break
|
||||
case 'updates':
|
||||
router.replace(path)
|
||||
setCurrentTab(AboutTabs.Updates)
|
||||
setCurrentPage(parts[1])
|
||||
break
|
||||
case 'roadmap':
|
||||
router.replace(path)
|
||||
setCurrentTab(AboutTabs.Roadmap)
|
||||
setCurrentPage(parts[1])
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const currentSection = () => {
|
||||
switch (currentTab) {
|
||||
case AboutTabs.About:
|
||||
return <AboutPage />
|
||||
case AboutTabs.Updates:
|
||||
return <UpdatesPage />
|
||||
case AboutTabs.Roadmap:
|
||||
return <RoadmapPage />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="About">
|
||||
<Head>
|
||||
{/* HTML */}
|
||||
<title>{t(`page.titles.${currentPage}`)}</title>
|
||||
<meta
|
||||
name="description"
|
||||
content={t(`page.descriptions.${currentPage}`)}
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
|
||||
{/* OpenGraph */}
|
||||
<meta property="og:title" content={t(`page.titles.${currentPage}`)} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={t('page.descriptions.about')}
|
||||
/>
|
||||
<meta
|
||||
property="og:url"
|
||||
content={`https://app.granblue.team/${currentPage}`}
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
|
||||
{/* Twitter */}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta property="twitter:domain" content="app.granblue.team" />
|
||||
<meta name="twitter:title" content={t(`page.titles.${currentPage}`)} />
|
||||
<meta
|
||||
name="twitter:description"
|
||||
content={t(`page.descriptions.${currentPage}`)}
|
||||
/>
|
||||
</Head>
|
||||
|
||||
<section>
|
||||
<SegmentedControl>
|
||||
<Segment
|
||||
groupName="about"
|
||||
name="about"
|
||||
selected={currentTab == AboutTabs.About}
|
||||
onClick={handleTabClicked}
|
||||
>
|
||||
{t('about.segmented_control.about')}
|
||||
</Segment>
|
||||
<Segment
|
||||
groupName="about"
|
||||
name="updates"
|
||||
selected={currentTab == AboutTabs.Updates}
|
||||
onClick={handleTabClicked}
|
||||
>
|
||||
{t('about.segmented_control.updates')}
|
||||
</Segment>
|
||||
<Segment
|
||||
groupName="about"
|
||||
name="roadmap"
|
||||
selected={currentTab == AboutTabs.Roadmap}
|
||||
onClick={handleTabClicked}
|
||||
>
|
||||
{t('about.segmented_control.roadmap')}
|
||||
</Segment>
|
||||
</SegmentedControl>
|
||||
{currentSection()}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const getServerSidePaths = async () => {
|
||||
return {
|
||||
paths: [],
|
||||
fallback: true,
|
||||
}
|
||||
}
|
||||
|
||||
// prettier-ignore
|
||||
export const getServerSideProps = async ({ req, res, locale, query }: { req: NextApiRequest, res: NextApiResponse, locale: string, query: { [index: string]: string } }) => {
|
||||
// Set headers for server-side requests
|
||||
setUserToken(req, res)
|
||||
|
||||
// Fetch and organize raids
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default AboutRoute
|
||||
|
|
@ -1,15 +1,19 @@
|
|||
import React, { useEffect } from 'react'
|
||||
import { useRouter } from 'next/router'
|
||||
import Head from 'next/head'
|
||||
import { useTranslation } from 'next-i18next'
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
||||
import clonedeep from 'lodash.clonedeep'
|
||||
|
||||
import Party from '~components/Party'
|
||||
|
||||
import { appState } from '~utils/appState'
|
||||
import { groupWeaponKeys } from '~utils/groupWeaponKeys'
|
||||
import api from '~utils/api'
|
||||
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
||||
import organizeRaids from '~utils/organizeRaids'
|
||||
import setUserToken from '~utils/setUserToken'
|
||||
import api from '~utils/api'
|
||||
import { appState, initialAppState } from '~utils/appState'
|
||||
import { groupWeaponKeys } from '~utils/groupWeaponKeys'
|
||||
import { printError } from '~utils/reportError'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import type { GroupedWeaponKeys } from '~utils/groupWeaponKeys'
|
||||
|
|
@ -20,12 +24,16 @@ interface Props {
|
|||
raids: Raid[]
|
||||
sortedRaids: Raid[][]
|
||||
weaponKeys: GroupedWeaponKeys
|
||||
version: AppUpdate
|
||||
}
|
||||
|
||||
const NewRoute: React.FC<Props> = (props: Props) => {
|
||||
// Import translations
|
||||
const { t } = useTranslation('common')
|
||||
|
||||
// Set up router
|
||||
const router = useRouter()
|
||||
|
||||
function callback(path: string) {
|
||||
// This is scuffed, how do we do this natively?
|
||||
window.history.replaceState(null, `Grid Tool`, `${path}`)
|
||||
|
|
@ -35,15 +43,26 @@ const NewRoute: React.FC<Props> = (props: Props) => {
|
|||
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() {
|
||||
appState.raids = props.raids
|
||||
appState.jobs = props.jobs
|
||||
appState.jobSkills = props.jobSkills
|
||||
appState.weaponKeys = props.weaponKeys
|
||||
appState.version = props.version
|
||||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<React.Fragment key={router.asPath}>
|
||||
<Head>
|
||||
{/* HTML */}
|
||||
<title>{t('page.titles.new')}</title>
|
||||
|
|
@ -82,32 +101,39 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Set headers for server-side requests
|
||||
setUserToken(req, res)
|
||||
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
.getAll()
|
||||
.then((response) => organizeRaids(response.data))
|
||||
try {
|
||||
// Fetch latest version
|
||||
const version = await fetchLatestVersion()
|
||||
|
||||
let jobs = await api.endpoints.jobs
|
||||
.getAll()
|
||||
.then((response) => {
|
||||
// Fetch and organize raids
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
.getAll()
|
||||
.then((response) => organizeRaids(response.data))
|
||||
|
||||
let jobs = await api.endpoints.jobs.getAll().then((response) => {
|
||||
return response.data
|
||||
})
|
||||
|
||||
let jobSkills = await api.allJobSkills().then((response) => response.data)
|
||||
let jobSkills = await api.allJobSkills().then((response) => response.data)
|
||||
|
||||
let weaponKeys = await api.endpoints.weapon_keys
|
||||
.getAll()
|
||||
.then((response) => groupWeaponKeys(response.data))
|
||||
let weaponKeys = await api.endpoints.weapon_keys
|
||||
.getAll()
|
||||
.then((response) => groupWeaponKeys(response.data))
|
||||
|
||||
return {
|
||||
props: {
|
||||
jobs: jobs,
|
||||
jobSkills: jobSkills,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
weaponKeys: weaponKeys,
|
||||
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
return {
|
||||
props: {
|
||||
jobs: jobs,
|
||||
jobSkills: jobSkills,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
weaponKeys: weaponKeys,
|
||||
version: version,
|
||||
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
printError(error, 'axios')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,17 +6,17 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
|
|||
|
||||
import Party from '~components/Party'
|
||||
|
||||
import { appState } from '~utils/appState'
|
||||
import { groupWeaponKeys } from '~utils/groupWeaponKeys'
|
||||
import api from '~utils/api'
|
||||
import generateTitle from '~utils/generateTitle'
|
||||
import organizeRaids from '~utils/organizeRaids'
|
||||
import setUserToken from '~utils/setUserToken'
|
||||
import api from '~utils/api'
|
||||
import { appState } from '~utils/appState'
|
||||
import { groupWeaponKeys } from '~utils/groupWeaponKeys'
|
||||
import { GridType } from '~utils/enums'
|
||||
import { printError } from '~utils/reportError'
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import type { GroupedWeaponKeys } from '~utils/groupWeaponKeys'
|
||||
import { useQueryState } from 'next-usequerystate'
|
||||
|
||||
interface Props {
|
||||
party: Party
|
||||
|
|
@ -70,7 +70,7 @@ const PartyRoute: React.FC<Props> = (props: Props) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<React.Fragment key={router.asPath}>
|
||||
<Party
|
||||
team={props.party}
|
||||
raids={props.sortedRaids}
|
||||
|
|
@ -154,35 +154,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Set headers for server-side requests
|
||||
setUserToken(req, res)
|
||||
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
.getAll()
|
||||
.then((response) => organizeRaids(response.data))
|
||||
|
||||
let jobs = await api.endpoints.jobs
|
||||
.getAll()
|
||||
.then((response) => {
|
||||
return response.data
|
||||
})
|
||||
|
||||
let jobSkills = await api
|
||||
.allJobSkills()
|
||||
.then((response) => response.data)
|
||||
|
||||
let weaponKeys = await api.endpoints.weapon_keys
|
||||
.getAll()
|
||||
.then((response) => groupWeaponKeys(response.data))
|
||||
|
||||
let party: Party | null = null
|
||||
if (query.party) {
|
||||
let response = await api.endpoints.parties.getOne({
|
||||
id: query.party
|
||||
})
|
||||
party = response.data.party
|
||||
} else {
|
||||
console.log('No party code')
|
||||
}
|
||||
|
||||
function getElement() {
|
||||
function getElement(party?: Party) {
|
||||
if (party) {
|
||||
const mainhand = party.weapons.find((weapon) => weapon.mainhand)
|
||||
if (mainhand && mainhand.object.element === 0) {
|
||||
|
|
@ -195,8 +167,8 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
}
|
||||
}
|
||||
|
||||
function elementEmoji() {
|
||||
const element = getElement()
|
||||
function elementEmoji(party?: Party) {
|
||||
const element = getElement(party)
|
||||
|
||||
if (element === 0) return '⚪'
|
||||
else if (element === 1) return '🟢'
|
||||
|
|
@ -208,20 +180,48 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
else return '⚪'
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
party: party,
|
||||
jobs: jobs,
|
||||
jobSkills: jobSkills,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
weaponKeys: weaponKeys,
|
||||
meta: {
|
||||
element: elementEmoji(),
|
||||
try {
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
.getAll()
|
||||
.then((response) => organizeRaids(response.data))
|
||||
|
||||
let jobs = await api.endpoints.jobs.getAll().then((response) => {
|
||||
return response.data
|
||||
})
|
||||
|
||||
let jobSkills = await api.allJobSkills().then((response) => response.data)
|
||||
|
||||
let weaponKeys = await api.endpoints.weapon_keys
|
||||
.getAll()
|
||||
.then((response) => groupWeaponKeys(response.data))
|
||||
|
||||
let party: Party | undefined = undefined
|
||||
if (query.party) {
|
||||
let response = await api.endpoints.parties.getOne({
|
||||
id: query.party,
|
||||
})
|
||||
party = response.data.party
|
||||
} else {
|
||||
console.log('No party code')
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
party: party,
|
||||
jobs: jobs,
|
||||
jobSkills: jobSkills,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
weaponKeys: weaponKeys,
|
||||
meta: {
|
||||
element: elementEmoji(party),
|
||||
},
|
||||
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
printError(error, 'axios')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,13 @@ import clonedeep from 'lodash.clonedeep'
|
|||
import api from '~utils/api'
|
||||
import setUserToken from '~utils/setUserToken'
|
||||
import extractFilters from '~utils/extractFilters'
|
||||
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
||||
import organizeRaids from '~utils/organizeRaids'
|
||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||
import { appState } from '~utils/appState'
|
||||
import { elements, allElement } from '~data/elements'
|
||||
import { emptyPaginationObject } from '~utils/emptyStates'
|
||||
import { printError } from '~utils/reportError'
|
||||
|
||||
import GridRep from '~components/GridRep'
|
||||
import GridRepCollection from '~components/GridRepCollection'
|
||||
|
|
@ -29,6 +32,7 @@ interface Props {
|
|||
meta: PaginationObject
|
||||
raids: Raid[]
|
||||
sortedRaids: Raid[][]
|
||||
version: AppUpdate
|
||||
}
|
||||
|
||||
const SavedRoute: React.FC<Props> = (props: Props) => {
|
||||
|
|
@ -97,6 +101,7 @@ const SavedRoute: React.FC<Props> = (props: Props) => {
|
|||
setTotalPages(props.meta.totalPages)
|
||||
setRecordCount(props.meta.count)
|
||||
replaceResults(props.meta.count, props.teams)
|
||||
appState.version = props.version
|
||||
}
|
||||
setCurrentPage(1)
|
||||
}, [])
|
||||
|
|
@ -352,39 +357,47 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Set headers for server-side requests
|
||||
setUserToken(req, res)
|
||||
|
||||
// Fetch and organize raids
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
.getAll()
|
||||
.then((response) => organizeRaids(response.data))
|
||||
try {
|
||||
// Fetch latest version
|
||||
const version = await fetchLatestVersion()
|
||||
|
||||
// Create filter object
|
||||
const filters: FilterObject = extractFilters(query, raids)
|
||||
const params = {
|
||||
params: { ...filters },
|
||||
}
|
||||
// Fetch and organize raids
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
.getAll()
|
||||
.then((response) => organizeRaids(response.data))
|
||||
|
||||
// Set up empty variables
|
||||
let teams: Party[] | null = null
|
||||
let meta: PaginationObject = emptyPaginationObject
|
||||
// Create filter object
|
||||
const filters: FilterObject = extractFilters(query, raids)
|
||||
const params = {
|
||||
params: { ...filters },
|
||||
}
|
||||
|
||||
// Fetch initial set of saved parties
|
||||
const response = await api.savedTeams(params)
|
||||
// Set up empty variables
|
||||
let teams: Party[] | null = null
|
||||
let meta: PaginationObject = emptyPaginationObject
|
||||
|
||||
// Assign values to pass to props
|
||||
teams = response.data.results
|
||||
meta.count = response.data.meta.count
|
||||
meta.totalPages = response.data.meta.total_pages
|
||||
meta.perPage = response.data.meta.per_page
|
||||
// Fetch initial set of saved parties
|
||||
const response = await api.savedTeams(params)
|
||||
|
||||
return {
|
||||
props: {
|
||||
teams: teams,
|
||||
meta: meta,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
// Assign values to pass to props
|
||||
teams = response.data.results
|
||||
meta.count = response.data.meta.count
|
||||
meta.totalPages = response.data.meta.total_pages
|
||||
meta.perPage = response.data.meta.per_page
|
||||
|
||||
return {
|
||||
props: {
|
||||
teams: teams,
|
||||
meta: meta,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
version: version,
|
||||
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
printError(error, 'axios')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,13 @@ import clonedeep from 'lodash.clonedeep'
|
|||
import api from '~utils/api'
|
||||
import setUserToken from '~utils/setUserToken'
|
||||
import extractFilters from '~utils/extractFilters'
|
||||
import fetchLatestVersion from '~utils/fetchLatestVersion'
|
||||
import organizeRaids from '~utils/organizeRaids'
|
||||
import useDidMountEffect from '~utils/useDidMountEffect'
|
||||
import { appState } from '~utils/appState'
|
||||
import { elements, allElement } from '~data/elements'
|
||||
import { emptyPaginationObject } from '~utils/emptyStates'
|
||||
import { printError } from '~utils/reportError'
|
||||
|
||||
import GridRep from '~components/GridRep'
|
||||
import GridRepCollection from '~components/GridRepCollection'
|
||||
|
|
@ -28,6 +31,7 @@ interface Props {
|
|||
teams?: Party[]
|
||||
meta: PaginationObject
|
||||
sortedRaids: Raid[][]
|
||||
version: AppUpdate
|
||||
}
|
||||
|
||||
const TeamsRoute: React.FC<Props> = (props: Props) => {
|
||||
|
|
@ -96,6 +100,7 @@ const TeamsRoute: React.FC<Props> = (props: Props) => {
|
|||
setTotalPages(props.meta.totalPages)
|
||||
setRecordCount(props.meta.count)
|
||||
replaceResults(props.meta.count, props.teams)
|
||||
appState.version = props.version
|
||||
}
|
||||
setCurrentPage(1)
|
||||
}, [])
|
||||
|
|
@ -363,39 +368,47 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
|
|||
// Set headers for server-side requests
|
||||
setUserToken(req, res)
|
||||
|
||||
// Fetch and organize raids
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
.getAll()
|
||||
.then((response) => organizeRaids(response.data))
|
||||
try {
|
||||
// Fetch latest version
|
||||
const version = await fetchLatestVersion()
|
||||
|
||||
// Create filter object
|
||||
const filters: FilterObject = extractFilters(query, raids)
|
||||
const params = {
|
||||
params: { ...filters },
|
||||
}
|
||||
// Fetch and organize raids
|
||||
let { raids, sortedRaids } = await api.endpoints.raids
|
||||
.getAll()
|
||||
.then((response) => organizeRaids(response.data))
|
||||
|
||||
// Set up empty variables
|
||||
let teams: Party[] | null = null
|
||||
let meta: PaginationObject = emptyPaginationObject
|
||||
// Create filter object
|
||||
const filters: FilterObject = extractFilters(query, raids)
|
||||
const params = {
|
||||
params: { ...filters },
|
||||
}
|
||||
|
||||
// Fetch initial set of parties
|
||||
const response = await api.endpoints.parties.getAll(params)
|
||||
// Set up empty variables
|
||||
let teams: Party[] | null = null
|
||||
let meta: PaginationObject = emptyPaginationObject
|
||||
|
||||
// Assign values to pass to props
|
||||
teams = response.data.results
|
||||
meta.count = response.data.meta.count
|
||||
meta.totalPages = response.data.meta.total_pages
|
||||
meta.perPage = response.data.meta.per_page
|
||||
// Fetch initial set of parties
|
||||
const response = await api.endpoints.parties.getAll(params)
|
||||
|
||||
return {
|
||||
props: {
|
||||
teams: teams,
|
||||
meta: meta,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
// Assign values to pass to props
|
||||
teams = response.data.results
|
||||
meta.count = response.data.meta.count
|
||||
meta.totalPages = response.data.meta.total_pages
|
||||
meta.perPage = response.data.meta.per_page
|
||||
|
||||
return {
|
||||
props: {
|
||||
teams: teams,
|
||||
meta: meta,
|
||||
raids: raids,
|
||||
sortedRaids: sortedRaids,
|
||||
version: version,
|
||||
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
|
||||
// Will be passed to the page component as props
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
printError(error, 'axios')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
3
public/icons/Manatura.svg
Normal file
3
public/icons/Manatura.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.49997 27.5C-0.500051 27.5 1.14279 17.8454 1.78667 15.9435C2.37392 14.2088 3.10735 12.4293 3.96802 10.8994C4.77642 9.46249 5.91051 7.85264 7.45945 6.92577C9.76438 5.54652 12.3986 5.18186 14.3985 4.905C14.8148 4.84736 15.2037 4.79353 15.5563 4.73515C17.7785 4.36726 19.8103 3.78856 21.9629 1.79762C22.9062 0.925157 24.3004 0.750323 25.43 1.36285C26.5595 1.97538 27.1736 3.23925 26.957 4.5058C26.1138 9.43587 23.0247 17.362 14.8822 19.8674C2.17635 23.7769 3.5 27.5 1.49997 27.5Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 612 B |
4
public/icons/Remix.svg
Normal file
4
public/icons/Remix.svg
Normal 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 |
3
public/icons/Shield.svg
Normal file
3
public/icons/Shield.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.2485 18.9009L23.2738 18.8461L23.2969 18.7903C25.0719 14.5024 25.7609 9.66488 25.9448 6.46207C26.0895 3.94417 24.3044 1.84216 21.9579 1.40085C15.9228 0.265829 11.8484 0.285157 6.02633 1.39844C3.68545 1.84606 1.90969 3.9468 2.05687 6.45935C2.23934 9.57444 2.91185 14.4595 4.70282 18.7895C5.52785 20.7841 6.67748 22.8599 8.06609 24.4951C9.34592 26.0023 11.3727 27.7692 14 27.7692C16.7045 27.7692 18.7211 25.8684 19.9427 24.385C21.2895 22.7497 22.4117 20.7123 23.2485 18.9009Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 610 B |
|
|
@ -1,4 +1,15 @@
|
|||
{
|
||||
"about": {
|
||||
"segmented_control": {
|
||||
"about": "About",
|
||||
"updates": "Updates",
|
||||
"roadmap": "Roadmap"
|
||||
}
|
||||
},
|
||||
"accessories": {
|
||||
"paladin": "shield",
|
||||
"manadiver": "manatura"
|
||||
},
|
||||
"alert": {
|
||||
"incompatible_weapon": "You've selected a weapon that can't be added to the Additional Weapon slots."
|
||||
},
|
||||
|
|
@ -28,6 +39,9 @@
|
|||
"show_info": "Edit info",
|
||||
"hide_info": "Hide info",
|
||||
"save_info": "Save info",
|
||||
"remix": "Remix",
|
||||
"save": "Save",
|
||||
"saved": "Saved",
|
||||
"menu": "Menu",
|
||||
"new": "New",
|
||||
"wiki": "View more on gbf.wiki"
|
||||
|
|
@ -40,6 +54,9 @@
|
|||
},
|
||||
"remove": "Remove from grid"
|
||||
},
|
||||
"errors": {
|
||||
"unauthorized": "You don't have permission to perform that action"
|
||||
},
|
||||
"filters": {
|
||||
"labels": {
|
||||
"element": "Element",
|
||||
|
|
@ -338,6 +355,9 @@
|
|||
},
|
||||
"page": {
|
||||
"titles": {
|
||||
"about": "About granblue.team",
|
||||
"updates": "Updates / granblue.team",
|
||||
"roadmap": "Roadmap / granblue.team",
|
||||
"discover": "Discover teams / granblue.team",
|
||||
"new": "Create a new team / granblue.team",
|
||||
"profile": "@{{username}}'s Teams / granblue.team",
|
||||
|
|
@ -345,6 +365,9 @@
|
|||
"saved": "Your saved teams / granblue.team"
|
||||
},
|
||||
"descriptions": {
|
||||
"about": "More about granblue.team / Save and discover teams to use in Granblue Fantasy",
|
||||
"updates": "Latest updates to granblue.team",
|
||||
"roadmap": "Upcoming planned features for granblue.team",
|
||||
"discover": "Save and discover teams to use in Granblue Fantasy and search by raid, element or recency",
|
||||
"new": "Create and theorycraft teams to use in Granblue Fantasy and share with the community",
|
||||
"profile": "Browse @{{username}}'s Teams and filter by raid, element or recency",
|
||||
|
|
@ -364,12 +387,24 @@
|
|||
"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",
|
||||
"equipped": "Equipped",
|
||||
"coming_soon": "Coming Soon",
|
||||
"new_party": "New party",
|
||||
"no_accessory": "No {{accessory}} equipped",
|
||||
"no_title": "Untitled",
|
||||
"no_raid": "No raid",
|
||||
"no_user": "Anonymous",
|
||||
"no_job": "No class",
|
||||
"no_value": "No value",
|
||||
"remixes": "Remixes",
|
||||
"level": "Level"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@
|
|||
"title": "Roadmap"
|
||||
}
|
||||
},
|
||||
"toasts": {
|
||||
"title": "New update",
|
||||
"description": {
|
||||
"content": "New items have been added from the latest Granblue Fantasy update.",
|
||||
"feature": "Now you can remix other people's teams, add Character rings and earrings, set Shields and Manatura on parties, and more!"
|
||||
},
|
||||
"button": "Learn more"
|
||||
},
|
||||
"title": "Roadmap",
|
||||
"subtitle": "Next update",
|
||||
"blurb": "I'm aiming for this update to release between late-January and early-February. I'm losing a week to top 2k in Guild Wars and after that I'm back at my full-time job, so progress will be a bit slower.",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,15 @@
|
|||
{
|
||||
"about": {
|
||||
"segmented_control": {
|
||||
"about": "サイトについて",
|
||||
"updates": "変更ログ",
|
||||
"roadmap": "ロードマップ"
|
||||
}
|
||||
},
|
||||
"accessories": {
|
||||
"paladin": "盾",
|
||||
"manadiver": "マナベリ"
|
||||
},
|
||||
"alert": {
|
||||
"incompatible_weapon": "Additional Weaponsに装備できない武器を入れました。"
|
||||
},
|
||||
|
|
@ -28,6 +39,9 @@
|
|||
"show_info": "詳細を編集",
|
||||
"save_info": "詳細を保存",
|
||||
"hide_info": "詳細を非表示",
|
||||
"remix": "リミックス",
|
||||
"save": "保存する",
|
||||
"saved": "保存",
|
||||
"menu": "メニュー",
|
||||
"new": "作成",
|
||||
"wiki": "gbf.wikiで詳しく見る"
|
||||
|
|
@ -48,6 +62,9 @@
|
|||
"rarity": "レアリティ"
|
||||
}
|
||||
},
|
||||
"errors": {
|
||||
"unauthorized": "行ったアクションを実行する権限がありません"
|
||||
},
|
||||
"header": {
|
||||
"anonymous": "無名",
|
||||
"untitled_team": "{{username}}さんからの無題編成",
|
||||
|
|
@ -339,6 +356,9 @@
|
|||
},
|
||||
"page": {
|
||||
"titles": {
|
||||
"about": "granblue.teamについて",
|
||||
"updates": "変更ログ / granblue.team",
|
||||
"roadmap": "ロードマップ / granblue.team",
|
||||
"discover": "編成を見出す / granblue.team",
|
||||
"new": "新しい編成 / granblue.team",
|
||||
"profile": "@{{username}}さんの作った編成 / granblue.team",
|
||||
|
|
@ -346,6 +366,9 @@
|
|||
"saved": "保存した編成"
|
||||
},
|
||||
"descriptions": {
|
||||
"about": "granblue.teamについて / グランブルーファンタジーの編成を探したり保存したりできる",
|
||||
"updates": "granblue.teamの最新変更について",
|
||||
"roadmap": "granblue.teamの開発予定機能",
|
||||
"discover": "グランブルーファンタジーの編成をマルチ、属性、作った時間などで探したり保存したりできる",
|
||||
"new": "グランブルーファンタジーの編成を作成し、騎空士とシェアできるサイトgranblue.team",
|
||||
"profile": "@{{username}}の編成を調査し、マルチ、属性、または作った時間でフィルターする",
|
||||
|
|
@ -365,12 +388,24 @@
|
|||
"no_skill": "設定されていません"
|
||||
}
|
||||
},
|
||||
"toasts": {
|
||||
"copied": "この編成のURLはクリップボードにコピーされました"
|
||||
},
|
||||
"tooltips": {
|
||||
"remix": "この編成をコピーする",
|
||||
"save": "この編成をアカウントに保存する",
|
||||
"source": "オリジナルの編成へ"
|
||||
},
|
||||
"equipped": "装備した",
|
||||
"extra_weapons": "Additional Weapons",
|
||||
"coming_soon": "開発中",
|
||||
"new_party": "新しい編成",
|
||||
"no_accessory": "{{accessory}}は装備していません",
|
||||
"no_title": "無題",
|
||||
"no_raid": "マルチなし",
|
||||
"no_user": "無名",
|
||||
"no_job": "ジョブなし",
|
||||
"no_value": "値なし",
|
||||
"remixes": "リミックスされた編成",
|
||||
"level": "レベル"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,14 @@
|
|||
"title": "ロードマップ"
|
||||
}
|
||||
},
|
||||
"toasts": {
|
||||
"title": "新アプデ",
|
||||
"description": {
|
||||
"content": "グランブルーファンタジーの新アプデのコンテンツが追加しました。",
|
||||
"feature": "編成をリミックスしたり、キャラの指輪や耳飾りを付けたり、盾やマナベリを装備したりことをできるようにしました。"
|
||||
},
|
||||
"button": "詳細をみる"
|
||||
},
|
||||
"title": "ロードマップ",
|
||||
"subtitle": "次回更新予定",
|
||||
"blurb": "1月下旬〜2月上旬に更新する予定があります。火古戦場に2000位を狙っており、その後は仕事に戻るので開発はちょっとだけ遅くなります。",
|
||||
|
|
|
|||
|
|
@ -119,6 +119,86 @@ select {
|
|||
}
|
||||
}
|
||||
|
||||
.PageContent {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $unit-4x;
|
||||
max-width: $grid-width;
|
||||
margin: $unit-4x auto 0;
|
||||
|
||||
h1 {
|
||||
font-size: $font-xxlarge;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $font-medium;
|
||||
font-weight: $medium;
|
||||
margin-bottom: $unit * 3;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-secondary);
|
||||
font-size: $font-regular;
|
||||
line-height: 1.3;
|
||||
margin-bottom: $unit;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.LinkItem {
|
||||
$diameter: $unit-6x;
|
||||
border: 1px solid var(--link-item-bg);
|
||||
border-radius: $card-corner;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--link-item-bg);
|
||||
|
||||
svg {
|
||||
fill: var(--link-item-image-color-hover);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
display: flex;
|
||||
padding: $unit-2x;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.Left {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: $unit-2x;
|
||||
flex-grow: 1;
|
||||
|
||||
h3 {
|
||||
font-weight: 600;
|
||||
max-width: 70%;
|
||||
line-height: 1.3;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: var(--link-item-image-color);
|
||||
width: $diameter;
|
||||
height: auto;
|
||||
|
||||
&.ShareIcon {
|
||||
width: $unit-4x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: $bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Hovercard {
|
||||
background: #222;
|
||||
border-radius: $unit;
|
||||
|
|
@ -288,6 +368,15 @@ i.tag {
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes openModalDesktop {
|
||||
0% {
|
||||
opacity: 0;
|
||||
|
|
@ -312,6 +401,24 @@ i.tag {
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes slideLeft {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideRight {
|
||||
from {
|
||||
transform: translateX(var(--radix-toast-swipe-end-x));
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInFilter {
|
||||
from {
|
||||
backdrop-filter: blur(5px) saturate(100%) brightness(80%) opacity(0);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// use with @include
|
||||
@mixin breakpoint($breakpoint) {
|
||||
$phone-width: 430px;
|
||||
$phone-width: 450px;
|
||||
$phone-height: 920px;
|
||||
|
||||
$tablet-width: 1024px;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@
|
|||
--accent-blue: #{$accent--blue--light};
|
||||
--accent-yellow: #{$accent--yellow--light};
|
||||
|
||||
--selected-item-bg: #{$selected--item--bg--light};
|
||||
--selected-item-bg-hover: #{$selected--item--bg--light--hover};
|
||||
|
||||
--placeholder-bg: #{$grey-80};
|
||||
|
||||
// Light - Menus
|
||||
--dialog-bg: #{$dialog--bg--light};
|
||||
|
||||
|
|
@ -143,6 +148,11 @@
|
|||
--accent-blue: #{$accent--blue--dark};
|
||||
--accent-yellow: #{$accent--yellow--dark};
|
||||
|
||||
--selected-item-bg: #{$selected--item--bg--dark};
|
||||
--selected-item-bg-hover: #{$selected--item--bg--dark--hover};
|
||||
|
||||
--placeholder-bg: #{$grey-40};
|
||||
|
||||
// Dark - Dialogs
|
||||
--dialog-bg: #{$dialog--bg--dark};
|
||||
|
||||
|
|
|
|||
|
|
@ -83,6 +83,11 @@ $accent--blue--dark: #6195f4;
|
|||
$accent--yellow--light: #c89d39;
|
||||
$accent--yellow--dark: #f9cc64;
|
||||
|
||||
$selected--item--bg--dark: #f9cc645d;
|
||||
$selected--item--bg--dark--hover: #fcc33f81;
|
||||
$selected--item--bg--light: #f9cc645d;
|
||||
$selected--item--bg--light--hover: #ecbc4c6f;
|
||||
|
||||
// Colors -- Elements
|
||||
$wind-text-00: #023e28;
|
||||
$wind-text-10: #006a43;
|
||||
|
|
@ -148,8 +153,8 @@ $dialog--bg--dark: $grey-25;
|
|||
// Color Definitions: Menu
|
||||
$menu--bg--light: $grey-100;
|
||||
$menu--bg--dark: $grey-10;
|
||||
$menu--text--light: $grey-90;
|
||||
$menu--text--dark: $grey-50;
|
||||
$menu--text--light: $grey-50;
|
||||
$menu--text--dark: $grey-60;
|
||||
$menu--separator--light: $grey-90;
|
||||
$menu--separator--dark: $grey-05;
|
||||
$menu--item--bg--light--hover: $grey-85;
|
||||
|
|
|
|||
5
types/AppUpdate.d.ts
vendored
Normal file
5
types/AppUpdate.d.ts
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
interface AppUpdate {
|
||||
version: string
|
||||
update_type: string
|
||||
updated_at: string
|
||||
}
|
||||
2
types/Job.d.ts
vendored
2
types/Job.d.ts
vendored
|
|
@ -14,4 +14,6 @@ interface Job {
|
|||
proficiency2: number
|
||||
}
|
||||
base_job?: Job
|
||||
accessory: boolean
|
||||
accessory_type: number
|
||||
}
|
||||
|
|
|
|||
11
types/JobAccessory.d.ts
vendored
Normal file
11
types/JobAccessory.d.ts
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
interface JobAccessory {
|
||||
id: string
|
||||
granblue_id: string
|
||||
job: Job
|
||||
name: {
|
||||
[key: string]: string
|
||||
en: string
|
||||
ja: string
|
||||
}
|
||||
rarity: number
|
||||
}
|
||||
4
types/Party.d.ts
vendored
4
types/Party.d.ts
vendored
|
|
@ -18,8 +18,10 @@ interface Party {
|
|||
button_count?: number
|
||||
turn_count?: number
|
||||
chain_count?: number
|
||||
source_party?: Party
|
||||
job: Job
|
||||
job_skills: JobSkillObject
|
||||
accessory: JobAccessory
|
||||
shortcode: string
|
||||
extra: boolean
|
||||
favorited: boolean
|
||||
|
|
@ -27,6 +29,8 @@ interface Party {
|
|||
weapons: Array<GridWeapon>
|
||||
summons: Array<GridSummon>
|
||||
user: User
|
||||
remix: boolean
|
||||
remixes: Party[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
|
|
|||
3
types/User.d.ts
vendored
3
types/User.d.ts
vendored
|
|
@ -1,11 +1,10 @@
|
|||
interface User {
|
||||
id: string
|
||||
username: string
|
||||
granblueId: number
|
||||
granblueId: string
|
||||
avatar: {
|
||||
picture: string
|
||||
element: string
|
||||
}
|
||||
gender: number
|
||||
private: boolean
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ import { proxy } from 'valtio'
|
|||
|
||||
export type UserState = {
|
||||
id: string
|
||||
granblueId: string
|
||||
username: string
|
||||
picture: string
|
||||
element: string
|
||||
avatar: {
|
||||
picture: string
|
||||
element: string
|
||||
}
|
||||
gender: number
|
||||
language: string
|
||||
theme: string
|
||||
|
|
|
|||
|
|
@ -115,6 +115,16 @@ class Api {
|
|||
return axios.get(resourceUrl, params)
|
||||
}
|
||||
|
||||
jobAccessoriesForJob(jobId: string, params?: {}) {
|
||||
const resourceUrl = `${this.url}/jobs/${jobId}/accessories`
|
||||
return axios.get(resourceUrl, params)
|
||||
}
|
||||
|
||||
remix(shortcode: string, params?: {}) {
|
||||
const resourceUrl = `${this.url}/parties/${shortcode}/remix`
|
||||
return axios.post(resourceUrl, params)
|
||||
}
|
||||
|
||||
savedTeams(params: {}) {
|
||||
const resourceUrl = `${this.url}/parties/favorites`
|
||||
return axios.get(resourceUrl, params)
|
||||
|
|
@ -148,6 +158,11 @@ class Api {
|
|||
const resourceUrl = `${this.url}/users/info/${id}`
|
||||
return axios.get(resourceUrl)
|
||||
}
|
||||
|
||||
version() {
|
||||
const resourceUrl = `${this.url}/version`
|
||||
return axios.get(resourceUrl)
|
||||
}
|
||||
}
|
||||
|
||||
const api: Api = new Api({ url: process.env.NEXT_PUBLIC_SIERO_API_URL || 'https://localhost:3000/api/v1'})
|
||||
|
|
|
|||
|
|
@ -16,6 +16,19 @@ const emptyJob: Job = {
|
|||
proficiency1: 0,
|
||||
proficiency2: 0,
|
||||
},
|
||||
accessory: false,
|
||||
accessory_type: 0,
|
||||
}
|
||||
|
||||
const emptyJobAccessory: JobAccessory = {
|
||||
id: '-1',
|
||||
granblue_id: '-1',
|
||||
job: emptyJob,
|
||||
name: {
|
||||
en: '',
|
||||
ja: '',
|
||||
},
|
||||
rarity: 0,
|
||||
}
|
||||
|
||||
interface AppState {
|
||||
|
|
@ -23,12 +36,14 @@ interface AppState {
|
|||
|
||||
party: {
|
||||
id: string | undefined
|
||||
shortcode: string | undefined
|
||||
editable: boolean
|
||||
detailsVisible: boolean
|
||||
name: string | undefined
|
||||
description: string | undefined
|
||||
job: Job
|
||||
jobSkills: JobSkillObject
|
||||
accessory: JobAccessory
|
||||
raid: Raid | undefined
|
||||
element: number
|
||||
fullAuto: boolean
|
||||
|
|
@ -41,6 +56,9 @@ interface AppState {
|
|||
extra: boolean
|
||||
user: User | undefined
|
||||
favorited: boolean
|
||||
remix: boolean
|
||||
remixes: Party[]
|
||||
sourceParty?: Party
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
|
@ -67,11 +85,13 @@ interface AppState {
|
|||
jobs: Job[]
|
||||
jobSkills: JobSkill[]
|
||||
weaponKeys: GroupedWeaponKeys
|
||||
version: AppUpdate
|
||||
}
|
||||
|
||||
export const initialAppState: AppState = {
|
||||
party: {
|
||||
id: undefined,
|
||||
shortcode: '',
|
||||
editable: false,
|
||||
detailsVisible: false,
|
||||
name: undefined,
|
||||
|
|
@ -83,6 +103,7 @@ export const initialAppState: AppState = {
|
|||
2: undefined,
|
||||
3: undefined,
|
||||
},
|
||||
accessory: emptyJobAccessory,
|
||||
raid: undefined,
|
||||
fullAuto: false,
|
||||
autoGuard: false,
|
||||
|
|
@ -95,6 +116,9 @@ export const initialAppState: AppState = {
|
|||
extra: false,
|
||||
user: undefined,
|
||||
favorited: false,
|
||||
remix: false,
|
||||
remixes: [],
|
||||
sourceParty: undefined,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
},
|
||||
|
|
@ -127,6 +151,11 @@ export const initialAppState: AppState = {
|
|||
gauph: [],
|
||||
emblem: [],
|
||||
},
|
||||
version: {
|
||||
version: '0.0',
|
||||
update_type: '',
|
||||
updated_at: '',
|
||||
},
|
||||
}
|
||||
|
||||
export const appState = proxy(initialAppState)
|
||||
|
|
|
|||
3
utils/capitalizeFirstLetter.tsx
Normal file
3
utils/capitalizeFirstLetter.tsx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export default function capitalizeFirstLetter(string: string) {
|
||||
return string.charAt(0).toUpperCase() + string.slice(1)
|
||||
}
|
||||
|
|
@ -6,7 +6,10 @@ export default function changeLanguage(
|
|||
newLanguage: string
|
||||
) {
|
||||
if (newLanguage !== router.locale) {
|
||||
setCookie('NEXT_LOCALE', newLanguage, { path: '/' })
|
||||
const expiresAt = new Date()
|
||||
expiresAt.setDate(expiresAt.getDate() + 60)
|
||||
|
||||
setCookie('NEXT_LOCALE', newLanguage, { path: '/', expires: expiresAt })
|
||||
router.push(router.asPath, undefined, { locale: newLanguage })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,3 +19,9 @@ export enum TeamElement {
|
|||
Dark,
|
||||
Light,
|
||||
}
|
||||
|
||||
export enum AboutTabs {
|
||||
About,
|
||||
Updates,
|
||||
Roadmap,
|
||||
}
|
||||
|
|
|
|||
10
utils/fetchLatestVersion.tsx
Normal file
10
utils/fetchLatestVersion.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import api from './api'
|
||||
|
||||
export default async function fetchLatestVersion() {
|
||||
try {
|
||||
const response = await api.version()
|
||||
return response.data as AppUpdate
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
4
utils/jobsWithAccessories.tsx
Normal file
4
utils/jobsWithAccessories.tsx
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export const ACCESSORY_JOB_IDS = [
|
||||
'683ffee8-4ea2-432d-bc30-4865020ac9f4',
|
||||
'a5d6fca3-5649-4e12-a6db-5fcec49150ee',
|
||||
]
|
||||
20
utils/reportError.tsx
Normal file
20
utils/reportError.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { AxiosError } from 'axios'
|
||||
|
||||
function handleError(error: any) {
|
||||
if (error instanceof Error) return error.message
|
||||
}
|
||||
|
||||
function handleAxiosError(error: any) {
|
||||
const axiosError = error as AxiosError
|
||||
return axiosError.response
|
||||
}
|
||||
|
||||
export function printError(error: any, type?: string) {
|
||||
if (type === 'axios') {
|
||||
const response = handleAxiosError(error)
|
||||
console.log(`${response?.status} ${response?.statusText}`)
|
||||
console.log(response?.data)
|
||||
} else {
|
||||
console.log(handleError(error))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue