Merge branch 'staging' into shields-manabelly

This commit is contained in:
Justin Edmund 2023-01-25 23:56:47 -08:00
commit 749ed4a7c3
42 changed files with 1188 additions and 722 deletions

View file

@ -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;
}
}
}

View file

@ -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&apos;re recruiting!) And yoey, but he won&apos;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

View file

@ -0,0 +1,11 @@
.About.PageContent {
.Links {
display: grid;
gap: $unit;
margin: $unit-2x 0;
}
div.LinkItem {
margin-top: $unit-2x;
}
}

View 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&apos;re
recruiting!) And yoey, but he won&apos;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

View file

@ -156,7 +156,9 @@ const AccountModal = (props: Props) => {
theme: user.theme,
}
setCookie('user', cookieObj, { path: '/' })
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + 60)
setCookie('user', cookieObj, { path: '/', expires: expiresAt })
accountState.account.user = {
id: user.id,

View file

@ -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

View file

@ -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 />

View 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;
}

View file

@ -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>
</>
)

View file

@ -133,7 +133,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,6 +146,9 @@ const LoginModal = () => {
const user = response.data
// Set user data in the user cookie
const expiresAt = new Date()
expiresAt.setDate(expiresAt.getDate() + 60)
setCookie(
'user',
{
@ -153,7 +158,7 @@ const LoginModal = () => {
gender: user.gender,
theme: user.theme,
},
{ path: '/' }
{ path: '/', expires: expiresAt }
)
// Set the user data in the account state

View file

@ -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;
}
}
}
}
}

View file

@ -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

View 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;
}
}
}
}

View 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

View file

@ -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)
}

View file

@ -91,7 +91,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,6 +108,9 @@ 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',
{
@ -115,7 +120,7 @@ const SignupModal = (props: Props) => {
gender: user.gender,
theme: user.theme,
},
{ path: '/' }
{ path: '/', expires: expiresAt }
)
// Set the user data in the account state

View file

@ -0,0 +1,35 @@
.Toast {
background: var(--dialog-bg);
border-radius: $card-corner;
box-shadow: 0 1px 12px rgba(0, 0, 0, 0.18);
display: flex;
flex-direction: column;
gap: $unit-2x;
padding: $unit-3x;
.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;
}
}

View file

@ -0,0 +1,48 @@
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 classes = classNames(props.className, {
Toast: true,
})
return (
<ToastPrimitive.Root {...props} className={classes}>
<div className="Header">
{title && (
<ToastPrimitive.Title asChild>
<h3>{title}</h3>
</ToastPrimitive.Title>
)}
<ToastPrimitive.Close aria-label="Close" onClick={props.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

View file

@ -0,0 +1,11 @@
.Notice {
align-items: center;
border-radius: $card-corner;
background: blue;
display: flex;
padding: $unit;
p {
font-size: $font-small;
}
}

View 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

View file

@ -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 {

View 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

View file

@ -27,6 +27,14 @@ module.exports = {
source: '/weapons',
destination: '/new',
},
{
source: '/updates',
destination: '/about',
},
{
source: '/roadmap',
destination: '/about',
},
{
source: '/p/:shortcode/characters',
destination: '/p/:shortcode',

18
package-lock.json generated
View file

@ -20,6 +20,7 @@
"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",
@ -3905,6 +3906,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",
@ -10037,6 +10050,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",

View file

@ -25,6 +25,7 @@
"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",

View file

@ -9,10 +9,13 @@ 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'
@ -30,6 +33,7 @@ interface Props {
meta: PaginationObject
raids: Raid[]
sortedRaids: Raid[][]
version: AppUpdate
}
const ProfileRoute: React.FC<Props> = (props: Props) => {
@ -99,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)
}, [])
@ -352,6 +357,9 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
setUserToken(req, res)
try {
// Fetch latest version
const version = await fetchLatestVersion()
// Fetch and organize raids
let { raids, sortedRaids } = await api.endpoints.raids
.getAll()
@ -393,6 +401,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
meta: meta,
raids: raids,
sortedRaids: sortedRaids,
version: version,
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
// Will be passed to the page component as props
},

View file

@ -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,7 @@ import { accountState } from '~utils/accountState'
import setUserToken from '~utils/setUserToken'
import '../styles/globals.scss'
import { ToastProvider, Viewport } from '@radix-ui/react-toast'
function MyApp({ Component, pageProps }: AppProps) {
const accountCookie = getCookie('account')
@ -43,9 +44,12 @@ function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<Layout>
<Component {...pageProps} />
</Layout>
<ToastProvider>
<Layout>
<Component {...pageProps} />
</Layout>
<Viewport className="ToastViewport" />
</ToastProvider>
</ThemeProvider>
)
}

174
pages/about.tsx Normal file
View 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

View file

@ -6,6 +6,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
import Party from '~components/Party'
import api from '~utils/api'
import fetchLatestVersion from '~utils/fetchLatestVersion'
import organizeRaids from '~utils/organizeRaids'
import setUserToken from '~utils/setUserToken'
import { appState } from '~utils/appState'
@ -21,6 +22,7 @@ interface Props {
raids: Raid[]
sortedRaids: Raid[][]
weaponKeys: GroupedWeaponKeys
version: AppUpdate
}
const NewRoute: React.FC<Props> = (props: Props) => {
@ -41,6 +43,7 @@ const NewRoute: React.FC<Props> = (props: Props) => {
appState.jobs = props.jobs
appState.jobSkills = props.jobSkills
appState.weaponKeys = props.weaponKeys
appState.version = props.version
}
return (
@ -84,6 +87,10 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
setUserToken(req, res)
try {
// Fetch latest version
const version = await fetchLatestVersion()
// Fetch and organize raids
let { raids, sortedRaids } = await api.endpoints.raids
.getAll()
.then((response) => organizeRaids(response.data))
@ -105,6 +112,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
raids: raids,
sortedRaids: sortedRaids,
weaponKeys: weaponKeys,
version: version,
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
// Will be passed to the page component as props
},

View file

@ -12,8 +12,10 @@ 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'
@ -30,6 +32,7 @@ interface Props {
meta: PaginationObject
raids: Raid[]
sortedRaids: Raid[][]
version: AppUpdate
}
const SavedRoute: React.FC<Props> = (props: Props) => {
@ -98,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)
}, [])
@ -354,6 +358,9 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
setUserToken(req, res)
try {
// Fetch latest version
const version = await fetchLatestVersion()
// Fetch and organize raids
let { raids, sortedRaids } = await api.endpoints.raids
.getAll()
@ -384,6 +391,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
meta: meta,
raids: raids,
sortedRaids: sortedRaids,
version: version,
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
// Will be passed to the page component as props
},

View file

@ -12,8 +12,10 @@ 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'
@ -29,6 +31,7 @@ interface Props {
teams?: Party[]
meta: PaginationObject
sortedRaids: Raid[][]
version: AppUpdate
}
const TeamsRoute: React.FC<Props> = (props: Props) => {
@ -97,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)
}, [])
@ -364,8 +368,11 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
// Set headers for server-side requests
setUserToken(req, res)
// Fetch and organize raids
try {
// Fetch latest version
const version = await fetchLatestVersion()
// Fetch and organize raids
let { raids, sortedRaids } = await api.endpoints.raids
.getAll()
.then((response) => organizeRaids(response.data))
@ -395,6 +402,7 @@ export const getServerSideProps = async ({ req, res, locale, query }: { req: Nex
meta: meta,
raids: raids,
sortedRaids: sortedRaids,
version: version,
...(await serverSideTranslations(locale, ['common', 'roadmap'])),
// Will be passed to the page component as props
},

View file

@ -1,4 +1,11 @@
{
"about": {
"segmented_control": {
"about": "About",
"updates": "Updates",
"roadmap": "Roadmap"
}
},
"alert": {
"incompatible_weapon": "You've selected a weapon that can't be added to the Additional Weapon slots."
},
@ -338,6 +345,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 +355,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",

View file

@ -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.",

View file

@ -1,4 +1,11 @@
{
"about": {
"segmented_control": {
"about": "サイトについて",
"updates": "変更ログ",
"roadmap": "ロードマップ"
}
},
"alert": {
"incompatible_weapon": "Additional Weaponsに装備できない武器を入れました。"
},
@ -339,6 +346,9 @@
},
"page": {
"titles": {
"about": "granblue.teamについて",
"updates": "変更ログ / granblue.team",
"roadmap": "ロードマップ / granblue.team",
"discover": "編成を見出す / granblue.team",
"new": "新しい編成 / granblue.team",
"profile": "@{{username}}さんの作った編成 / granblue.team",
@ -346,6 +356,9 @@
"saved": "保存した編成"
},
"descriptions": {
"about": "granblue.teamについて / グランブルーファンタジーの編成を探したり保存したりできる",
"updates": "granblue.teamの最新変更について",
"roadmap": "granblue.teamの開発予定機能",
"discover": "グランブルーファンタジーの編成をマルチ、属性、作った時間などで探したり保存したりできる",
"new": "グランブルーファンタジーの編成を作成し、騎空士とシェアできるサイトgranblue.team",
"profile": "@{{username}}の編成を調査し、マルチ、属性、または作った時間でフィルターする",

View file

@ -4,6 +4,14 @@
"title": "ロードマップ"
}
},
"toasts": {
"title": "新アプデ",
"description": {
"content": "グランブルーファンタジーの新アプデのコンテンツが追加しました。",
"feature": "編成をリミックスしたり、キャラの指輪や耳飾りを付けたり、盾やマナベリを装備したりことをできるようにしました。"
},
"button": "詳細をみる"
},
"title": "ロードマップ",
"subtitle": "次回更新予定",
"blurb": "1月下旬〜2月上旬に更新する予定があります。火古戦場に2000位を狙っており、その後は仕事に戻るので開発はちょっとだけ遅くなります。",

View file

@ -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;

5
types/AppUpdate.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
interface AppUpdate {
version: string
update_type: string
updated_at: string
}

View file

@ -153,6 +153,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'})

View file

@ -79,6 +79,7 @@ interface AppState {
jobs: Job[]
jobSkills: JobSkill[]
weaponKeys: GroupedWeaponKeys
version: AppUpdate
}
export const initialAppState: AppState = {
@ -140,6 +141,11 @@ export const initialAppState: AppState = {
gauph: [],
emblem: [],
},
version: {
version: '0.0',
update_type: '',
updated_at: '',
},
}
export const appState = proxy(initialAppState)

View file

@ -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 })
}
}

View file

@ -19,3 +19,9 @@ export enum TeamElement {
Dark,
Light,
}
export enum AboutTabs {
About,
Updates,
Roadmap,
}

View 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)
}
}