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(() => {
const cookie = getToastCookie() if (appState.version) {
const now = new Date() const cookie = getToastCookie()
const updatedAt = new Date(appState.version.updated_at) const now = new Date()
const validUntil = add(updatedAt, { days: 7 }) const updatedAt = new Date(appState.version.updated_at)
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,23 +52,32 @@ 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 (
<UpdateToast !['about', 'updates', 'roadmap'].includes(path) &&
open={updateToastOpen} appState.version && (
updateType={appState.version.update_type} <UpdateToast
onActionClicked={handleToastActionClicked} open={updateToastOpen}
onCloseClicked={handleToastClosed} updateType={appState.version.update_type}
lastUpdated={appState.version.updated_at} onActionClicked={handleToastActionClicked}
/> onCloseClicked={handleToastClosed}
) : ( 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>
<Component {...pageProps} /> {appState.version ? (
serverUnavailable()
) : (
<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,6 +152,188 @@ select {
line-height: 1.3; line-height: 1.3;
} }
#Teams,
#Profile {
display: flex;
height: 100%;
flex-direction: column;
gap: $unit * 2;
}
#NotFound {
height: 200px;
width: 400px;
margin: auto;
display: flex;
justify-content: center;
align-items: center;
h2 {
color: $grey-60;
font-size: $font-regular;
text-align: center;
}
}
img.profile {
background: $grey-90;
&.fire {
background: $fire-bg-20;
}
&.water {
background: $water-bg-20;
}
&.wind {
background: $wind-bg-20;
}
&.earth {
background: $earth-bg-20;
}
&.dark {
background: $dark-bg-10;
}
&.light {
background: $light-bg-20;
}
&.anonymous {
background: var(--anonymous-bg);
}
}
i.tag {
background: var(--tag-bg);
color: var(--tag-text);
border-radius: calc($unit / 2);
font-size: 10px;
font-weight: $bold;
padding: 4px 6px;
text-transform: uppercase;
}
.infinite-scroll-component {
overflow: hidden !important;
}
.SearchFilterBar {
display: flex;
gap: $unit;
padding: 0 ($unit * 3);
@include breakpoint(phone) {
display: grid;
gap: 8px;
grid-template-columns: 1fr 1fr;
}
}
.Joined {
$offset: 2px;
align-items: center;
background: var(--input-bg);
border-radius: $input-corner;
border: $offset solid transparent;
box-sizing: border-box;
display: flex;
gap: $unit;
padding-top: 2px;
padding-bottom: 2px;
padding-right: calc($unit-2x - $offset);
&.Bound {
background-color: var(--input-bound-bg);
&:hover {
background-color: var(--input-bound-bg-hover);
}
}
&:focus-within {
border: $offset solid $blue;
// box-shadow: 0 2px rgba(255, 255, 255, 1);
}
.Counter {
color: $grey-55;
font-weight: $bold;
line-height: 42px;
}
.Input {
background: transparent;
border: none;
border-radius: 0;
padding: $unit * 1.5 $unit-2x;
padding-left: calc($unit-2x - $offset);
&:focus {
border: none;
outline: none;
}
}
}
}
.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 { .LinkItem {
$diameter: $unit-6x; $diameter: $unit-6x;
align-items: center; align-items: center;
@ -225,131 +407,3 @@ select {
} }
} }
} }
#Teams,
#Profile {
display: flex;
height: 100%;
flex-direction: column;
gap: $unit * 2;
}
#NotFound {
height: 200px;
width: 400px;
margin: auto;
display: flex;
justify-content: center;
align-items: center;
h2 {
color: $grey-60;
font-size: $font-regular;
text-align: center;
}
}
img.profile {
background: $grey-90;
&.fire {
background: $fire-bg-20;
}
&.water {
background: $water-bg-20;
}
&.wind {
background: $wind-bg-20;
}
&.earth {
background: $earth-bg-20;
}
&.dark {
background: $dark-bg-10;
}
&.light {
background: $light-bg-20;
}
&.anonymous {
background: var(--anonymous-bg);
}
}
i.tag {
background: var(--tag-bg);
color: var(--tag-text);
border-radius: calc($unit / 2);
font-size: 10px;
font-weight: $bold;
padding: 4px 6px;
text-transform: uppercase;
}
.infinite-scroll-component {
overflow: hidden !important;
}
.SearchFilterBar {
display: flex;
gap: $unit;
padding: 0 ($unit * 3);
@include breakpoint(phone) {
display: grid;
gap: 8px;
grid-template-columns: 1fr 1fr;
}
}
.Joined {
$offset: 2px;
align-items: center;
background: var(--input-bg);
border-radius: $input-corner;
border: $offset solid transparent;
box-sizing: border-box;
display: flex;
gap: $unit;
padding-top: 2px;
padding-bottom: 2px;
padding-right: calc($unit-2x - $offset);
&.Bound {
background-color: var(--input-bound-bg);
&:hover {
background-color: var(--input-bound-bg-hover);
}
}
&:focus-within {
border: $offset solid $blue;
// box-shadow: 0 2px rgba(255, 255, 255, 1);
}
.Counter {
color: $grey-55;
font-weight: $bold;
line-height: 42px;
}
.Input {
background: transparent;
border: none;
border-radius: 0;
padding: $unit * 1.5 $unit-2x;
padding-left: calc($unit-2x - $offset);
&:focus {
border: none;
outline: none;
}
}
}

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