Merge branch 'staging' into shields-manabelly
This commit is contained in:
commit
749ed4a7c3
42 changed files with 1188 additions and 722 deletions
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
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>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
35
components/Toast/index.scss
Normal file
35
components/Toast/index.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
48
components/Toast/index.tsx
Normal file
48
components/Toast/index.tsx
Normal 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
|
||||
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
|
||||
|
|
@ -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
18
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<ToastProvider>
|
||||
<Layout>
|
||||
<Component {...pageProps} />
|
||||
</Layout>
|
||||
<Viewport className="ToastViewport" />
|
||||
</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
|
||||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,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}}の編成を調査し、マルチ、属性、または作った時間でフィルターする",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
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
|
||||
}
|
||||
|
|
@ -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'})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue