Add a message if the server goes down

Right now the app fails silently if the server becomes unreachable. Now, we use the version check to determine if we have a connection to the server, and if not we display an error message.
This commit is contained in:
Justin Edmund 2023-06-22 01:49:10 -07:00
parent 9de87abd1e
commit ab671b509d
6 changed files with 264 additions and 148 deletions

View file

@ -17,16 +17,18 @@ const Layout = ({ children }: PropsWithChildren<Props>) => {
const [updateToastOpen, setUpdateToastOpen] = useState(false) const [updateToastOpen, setUpdateToastOpen] = useState(false)
useEffect(() => { useEffect(() => {
if (appState.version) {
const cookie = getToastCookie() const cookie = getToastCookie()
const now = new Date() const now = new Date()
const updatedAt = new Date(appState.version.updated_at) const updatedAt = new Date(appState.version.updated_at)
const validUntil = add(updatedAt, { days: 7 }) const validUntil = add(updatedAt, { days: 7 })
if (now < validUntil && !cookie.seen) setUpdateToastOpen(true) if (now < validUntil && !cookie.seen) setUpdateToastOpen(true)
}
}, []) }, [])
function getToastCookie() { function getToastCookie() {
if (appState.version.updated_at !== '') { if (appState.version && appState.version.updated_at !== '') {
const updatedAt = new Date(appState.version.updated_at) const updatedAt = new Date(appState.version.updated_at)
const cookieValues = getCookie( const cookieValues = getCookie(
`update-${format(updatedAt, 'yyyy-MM-dd')}` `update-${format(updatedAt, 'yyyy-MM-dd')}`
@ -50,7 +52,9 @@ const Layout = ({ children }: PropsWithChildren<Props>) => {
const updateToast = () => { const updateToast = () => {
const path = router.asPath.replaceAll('/', '') const path = router.asPath.replaceAll('/', '')
return !['about', 'updates', 'roadmap'].includes(path) ? ( return (
!['about', 'updates', 'roadmap'].includes(path) &&
appState.version && (
<UpdateToast <UpdateToast
open={updateToastOpen} open={updateToastOpen}
updateType={appState.version.update_type} updateType={appState.version.update_type}
@ -58,15 +62,22 @@ const Layout = ({ children }: PropsWithChildren<Props>) => {
onCloseClicked={handleToastClosed} onCloseClicked={handleToastClosed}
lastUpdated={appState.version.updated_at} lastUpdated={appState.version.updated_at}
/> />
) : ( )
'' )
}
const ServerAvailable = () => {
return (
<>
<TopHeader />
{updateToast()}
</>
) )
} }
return ( return (
<> <>
<TopHeader /> {appState.version && ServerAvailable}
{updateToast()}
<main>{children}</main> <main>{children}</main>
</> </>
) )

View file

@ -1,10 +1,13 @@
import { appWithTranslation } from 'next-i18next' import { appWithTranslation } from 'next-i18next'
import Link from 'next/link'
import { useTranslation } from 'next-i18next'
import { get } from 'local-storage' import { get } from 'local-storage'
import { getCookie, setCookie } from 'cookies-next' import { getCookie, setCookie } from 'cookies-next'
import { subscribe } from 'valtio' import { subscribe } from 'valtio'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { ThemeProvider } from 'next-themes' import { ThemeProvider } from 'next-themes'
import { appState } from '~utils/appState'
import { accountState } from '~utils/accountState' import { accountState } from '~utils/accountState'
import { retrieveCookies } from '~utils/retrieveCookies' import { retrieveCookies } from '~utils/retrieveCookies'
import { setHeaders } from '~utils/userToken' import { setHeaders } from '~utils/userToken'
@ -16,9 +19,12 @@ import Layout from '~components/Layout'
import type { AppProps } from 'next/app' import type { AppProps } from 'next/app'
import DiscordIcon from '~public/icons/discord.svg'
import ShareIcon from '~public/icons/Share.svg'
import '../styles/globals.scss' import '../styles/globals.scss'
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
const { t } = useTranslation('common')
const [refresh, setRefresh] = useState(false) const [refresh, setRefresh] = useState(false)
// Subscribe to app state to listen for account changes and // Subscribe to app state to listen for account changes and
@ -88,12 +94,47 @@ function MyApp({ Component, pageProps }: AppProps) {
} }
} }
const serverUnavailable = () => {
return (
<div className="ServerUnavailableWrapper">
<div className="ServerUnavailable">
<div className="Message">
<h1>{t('errors.server_unavailable.title')}</h1>
<p>{t('errors.server_unavailable.message')}</p>
</div>
<div className="Connect">
<p>{t('errors.server_unavailable.discord')}</p>
<div className="Discord 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>
</div>
</div>
</div>
)
}
return ( return (
<ThemeProvider> <ThemeProvider>
<ToastProvider swipeDirection="right"> <ToastProvider swipeDirection="right">
<TooltipProvider> <TooltipProvider>
<Layout> <Layout>
{appState.version ? (
serverUnavailable()
) : (
<Component {...pageProps} /> <Component {...pageProps} />
)}
</Layout> </Layout>
<Viewport className="ToastViewport" /> <Viewport className="ToastViewport" />
</TooltipProvider> </TooltipProvider>

View file

@ -87,6 +87,11 @@
"description": "The page you're looking for couldn't be found", "description": "The page you're looking for couldn't be found",
"button": "Create a new party" "button": "Create a new party"
}, },
"server_unavailable": {
"title": "Server unavailable",
"message": "We're having trouble connecting to the server right now",
"discord": "Join us on Discord for status updates"
},
"validation": { "validation": {
"guidebooks": "You cannot equip more than one of each Sephira Guidebook" "guidebooks": "You cannot equip more than one of each Sephira Guidebook"
}, },

View file

@ -87,6 +87,11 @@
"description": "探しているページは見つかりませんでした", "description": "探しているページは見つかりませんでした",
"button": "新しい編成を作成" "button": "新しい編成を作成"
}, },
"server_unavailable": {
"title": "サーバーダウン",
"message": "現在サーバーがダウンしています。しばらくしてから再度お試しください",
"discord": "Discordでサービスの状態を確認する"
},
"validation": { "validation": {
"guidebooks": "セフィラ導本を複数個装備することはできません" "guidebooks": "セフィラ導本を複数個装備することはできません"
}, },

View file

@ -152,80 +152,6 @@ select {
line-height: 1.3; line-height: 1.3;
} }
.LinkItem {
$diameter: $unit-6x;
align-items: center;
background: var(--dialog-bg);
border: 1px solid var(--link-item-bg);
border-radius: $card-corner;
display: flex;
min-height: 82px;
transition: background $duration-zoom ease-in,
transform $duration-zoom ease-in;
&:hover {
background: var(--link-item-bg);
color: var(--text-primary);
.ShareIcon {
fill: var(--text-primary);
transform: translate($unit-half, calc(($unit * -1) / 2));
}
}
&.Github {
&:hover {
.Left svg {
fill: var(--text-primary);
}
}
}
&.Discord:hover .Left svg {
fill: #5865f2;
}
a {
display: flex;
justify-content: space-between;
padding: $unit-2x;
width: 100%;
&: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;
transition: fill $duration-zoom ease-in;
&.ShareIcon {
width: $unit-4x;
}
}
}
h3 {
font-weight: $bold;
}
}
}
#Teams, #Teams,
#Profile { #Profile {
display: flex; display: flex;
@ -353,3 +279,131 @@ i.tag {
} }
} }
} }
}
.ServerUnavailableWrapper {
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
.ServerUnavailable {
align-items: center;
display: flex;
flex-direction: column;
gap: $unit-6x;
margin: auto;
max-width: 440px;
.Message {
display: flex;
flex-direction: column;
gap: $unit;
h1 {
color: var(--text-primary);
font-size: $font-xxlarge;
font-weight: $bold;
text-align: center;
}
p {
color: var(--text-secondary);
font-size: $font-regular;
font-weight: $bold;
text-align: center;
}
}
.Connect {
color: var(--text-tertiary);
display: flex;
flex-direction: column;
justify-content: center;
gap: $unit-2x;
width: 100%;
p {
text-align: center;
}
.LinkItem {
width: 100%;
}
}
}
.LinkItem {
$diameter: $unit-6x;
align-items: center;
background: var(--dialog-bg);
border: 1px solid var(--link-item-bg);
border-radius: $card-corner;
display: flex;
min-height: 82px;
transition: background $duration-zoom ease-in,
transform $duration-zoom ease-in;
&:hover {
background: var(--link-item-bg);
color: var(--text-primary);
.ShareIcon {
fill: var(--text-primary);
transform: translate($unit-half, calc(($unit * -1) / 2));
}
}
&.Github {
&:hover {
.Left svg {
fill: var(--text-primary);
}
}
}
&.Discord:hover .Left svg {
fill: #5865f2;
}
a {
display: flex;
justify-content: space-between;
padding: $unit-2x;
width: 100%;
&: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;
transition: fill $duration-zoom ease-in;
&.ShareIcon {
width: $unit-4x;
}
}
}
h3 {
font-weight: $bold;
}
}
}

View file

@ -88,7 +88,7 @@ interface AppState {
jobs: Job[] jobs: Job[]
jobSkills: JobSkill[] jobSkills: JobSkill[]
weaponKeys: GroupedWeaponKeys weaponKeys: GroupedWeaponKeys
version: AppUpdate version?: AppUpdate
status?: ResponseStatus status?: ResponseStatus
} }