web/satellite: update Vue error page design

A common error page component that reflects our new designs has been
added, replacing our old 404 and 50X Vue pages.

Change-Id: Ifc9071674eeda03c5d961c26dce9ff0c80b95c6e
This commit is contained in:
Jeremy Wharton 2023-04-13 06:19:16 -05:00
parent f076238748
commit 49c5e3de9e
12 changed files with 229 additions and 188 deletions

View File

@ -4,6 +4,7 @@
<template>
<div id="app">
<BrandedLoader v-if="isLoading" />
<ErrorPage v-else-if="isErrorPageShown" />
<router-view v-else />
<!-- Area for displaying notification -->
<NotificationArea />
@ -11,12 +12,13 @@
</template>
<script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue';
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
import { PartneredSatellite } from '@/types/common';
import { MetaUtils } from '@/utils/meta';
import { useNotify } from '@/utils/hooks';
import { useAppStore } from '@/store/modules/appStore';
import ErrorPage from '@/views/ErrorPage.vue';
import BrandedLoader from '@/components/common/BrandedLoader.vue';
import NotificationArea from '@/components/notifications/NotificationArea.vue';
@ -26,6 +28,13 @@ const notify = useNotify();
const isLoading = ref<boolean>(true);
/**
* Indicates whether an error page should be shown in place of the router view.
*/
const isErrorPageShown = computed((): boolean => {
return appStore.state.viewsState.error.visible;
});
/**
* Fixes the issue where view port height is taller than the visible viewport on
* mobile Safari/Webkit. See: https://bugs.webkit.org/show_bug.cgi?id=141832

View File

@ -1,58 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template src="./page40X.html" />
<script lang="ts">
import { defineComponent } from 'vue';
import LogoIcon from '@/../static/images/logo.svg';
import MainIcon from '@/../static/images/errors/404.svg';
export default defineComponent({
name: 'Page404',
components: {
LogoIcon,
MainIcon,
},
setup() {},
});
</script>
<style scoped lang="scss">
.error-container {
position: fixed;
top: 0;
left: 0;
z-index: 1000;
width: 100%;
height: 100vh;
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
-webkit-touch-callout: none;
user-select: none;
&__title {
font-family: 'font_regular', sans-serif;
margin-bottom: 60px;
font-size: 32px;
}
}
.logo {
display: flex;
align-items: center;
position: absolute;
top: 87px;
left: 100px;
width: 207px;
height: 37px;
}
.text {
margin-left: 15px;
}
</style>

View File

@ -1,63 +0,0 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template src="./page50X.html" />
<script lang="ts">
import { defineComponent } from 'vue';
import LogoIcon from '@/../static/images/logo.svg';
import MainIcon from '@/../static/images/errors/50X.svg';
export default defineComponent({
name: 'Page50X',
components: {
LogoIcon,
MainIcon,
},
setup() {},
});
</script>
<style scoped lang="scss">
.error-container {
position: fixed;
top: 0;
left: 0;
z-index: 1000;
width: 100%;
height: 100vh;
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
-webkit-touch-callout: none;
user-select: none;
&__title {
font-family: 'font_regular', sans-serif;
margin-bottom: 60px;
font-size: 32px;
}
img,
a {
-webkit-user-drag: none;
}
}
.logo {
display: flex;
align-items: center;
position: absolute;
top: 87px;
left: 100px;
width: 207px;
height: 37px;
}
.text {
margin-left: 15px;
}
</style>

View File

@ -1,10 +0,0 @@
<!--Copyright (C) 2019 Storj Labs, Inc.-->
<!--See LICENSE for copying information.-->
<div class="error-container">
<a href="/" class="logo">
<LogoIcon class="header-container__left-area__logo__img"/>
</a>
<h1 class="error-container__title">404. Something Went Wrong</h1>
<MainIcon/>
</div>

View File

@ -1,10 +0,0 @@
<!--Copyright (C) 2019 Storj Labs, Inc.-->
<!--See LICENSE for copying information.-->
<div class="error-container">
<a href="/" class="logo">
<LogoIcon class="header-container__left-area__logo__img"/>
</a>
<h1 class="error-container__title">503 Unknown Error</h1>
<MainIcon/>
</div>

View File

@ -417,7 +417,7 @@ onMounted(async (): Promise<void> => {
appStore.updateActiveModal(MODALS.createProjectPassphrase);
}
appStore.toggleHasJustLoggenIn();
appStore.toggleHasJustLoggedIn();
}
await store.dispatch(PROJECTS_ACTIONS.FETCH_DAILY_DATA, { since: past, before: now });

View File

@ -3,7 +3,7 @@
import Vue from 'vue';
import VueClipboard from 'vue-clipboard2';
import { createPinia, PiniaVuePlugin } from 'pinia';
import { createPinia, setActivePinia, PiniaVuePlugin } from 'pinia';
import App from './App.vue';
import { router } from './router';
@ -24,6 +24,7 @@ Vue.use(new NotificatorPlugin(store));
Vue.use(VueClipboard);
Vue.use(PiniaVuePlugin);
const pinia = createPinia();
setActivePinia(pinia);
/**
* Click outside handlers.

View File

@ -17,7 +17,6 @@ import BillingOverview from '@/components/account/billing/billingTabs/Overview.v
import BillingPaymentMethods from '@/components/account/billing/billingTabs/PaymentMethods.vue';
import BillingHistory from '@/components/account/billing/billingTabs/BillingHistory.vue';
import BillingCoupons from '@/components/account/billing/billingTabs/Coupons.vue';
import Page404 from '@/components/errors/Page404.vue';
import BucketsView from '@/components/objects/BucketsView.vue';
import ObjectsArea from '@/components/objects/ObjectsArea.vue';
import UploadFile from '@/components/objects/UploadFile.vue';
@ -436,10 +435,5 @@ export const router = new Router({
},
],
},
{
path: '*',
name: '404',
component: Page404,
},
],
});

View File

@ -40,20 +40,11 @@ export const store = new Vuex.Store<ModulesState>({
},
});
store.subscribe((mutation, state) => {
const currentRouteName = router.currentRoute.name;
const appStore = useAppStore();
const satelliteName = appStore.state.satelliteName;
store.subscribe((mutation) => {
switch (mutation.type) {
case PROJECTS_MUTATIONS.REMOVE:
document.title = `${router.currentRoute.name} | ${satelliteName}`;
break;
case PROJECTS_MUTATIONS.SELECT_PROJECT:
if (currentRouteName && !notProjectRelatedRoutes.includes(currentRouteName)) {
document.title = `${state.projectsModule.selectedProject.name} | ${currentRouteName} | ${satelliteName}`;
}
updateTitle();
}
});
@ -65,14 +56,21 @@ export default store;
relies on store state for the routing behavior.
*/
router.beforeEach(async (to, from, next) => {
const appStore = useAppStore();
if (!to.matched.length) {
appStore.setErrorPage(404);
return;
} else if (appStore.state.viewsState.error.visible) {
appStore.removeErrorPage();
}
if (to.name === RouteConfig.ProjectDashboard.name && from.name === RouteConfig.Login.name) {
const appStore = useAppStore();
appStore.toggleHasJustLoggenIn(true);
appStore.toggleHasJustLoggedIn(true);
}
if (to.name === RouteConfig.AllProjectsDashboard.name && from.name === RouteConfig.Login.name) {
const appStore = useAppStore();
appStore.toggleHasJustLoggenIn(true);
appStore.toggleHasJustLoggedIn(true);
}
// On very first login we try to redirect user to project dashboard
@ -80,12 +78,10 @@ router.beforeEach(async (to, from, next) => {
// That's why we toggle this flag here back to false not show create project passphrase modal again
// if user clicks 'Continue in web'.
if (to.name === RouteConfig.ProjectDashboard.name && from.name === RouteConfig.OverviewStep.name) {
const appStore = useAppStore();
appStore.toggleHasJustLoggenIn(false);
appStore.toggleHasJustLoggedIn(false);
}
if (to.name === RouteConfig.ProjectDashboard.name && from.name === RouteConfig.AllProjectsDashboard.name) {
const appStore = useAppStore();
appStore.toggleHasJustLoggenIn(false);
appStore.toggleHasJustLoggedIn(false);
}
// TODO: I disabled this navigation guard because we try to get active pinia before it is initialised.
@ -132,23 +128,8 @@ router.beforeEach(async (to, from, next) => {
next();
});
router.afterEach(({ name }, _from) => {
if (!name) {
return;
}
const appStore = useAppStore();
if (notProjectRelatedRoutes.includes(name)) {
document.title = `${router.currentRoute.name} | ${appStore.state.satelliteName}`;
return;
}
const selectedProjectName = store.state.projectsModule.selectedProject.name ?
`${store.state.projectsModule.selectedProject.name} | ` : '';
document.title = `${selectedProjectName + router.currentRoute.name} | ${appStore.state.satelliteName}`;
router.afterEach(() => {
updateTitle();
});
/**
@ -161,3 +142,18 @@ function navigateToDefaultSubTab(routes: RouteRecord[], tabRoute: NavigationLink
return (routes.length === 2 && (routes[1].name as string) === tabRoute.name) ||
(routes.length === 3 && (routes[2].name as string) === tabRoute.name);
}
/**
* Updates the title of the webpage.
*/
function updateTitle(): void {
const appStore = useAppStore();
const routeName = router.currentRoute.name;
const parts = [routeName, appStore.state.satelliteName];
if (routeName && !notProjectRelatedRoutes.includes(routeName)) {
parts.unshift(store.state.projectsModule.selectedProject.name);
}
document.title = parts.filter(s => !!s).join(' | ');
}

View File

@ -31,6 +31,15 @@ class ViewsState {
// this field is mainly used on the all projects dashboard as an exit condition
// for when the dashboard opens the pricing plan and the pricing plan navigates back repeatedly.
public hasShownPricingPlan = false;
public error: ErrorPageState = new ErrorPageState();
}
class ErrorPageState {
constructor(
public statusCode = 0,
public fatal = false,
public visible = false,
) {}
}
export class State {
@ -155,6 +164,14 @@ export const useAppStore = defineStore('app', () => {
state.viewsState.activeDropdown = '';
}
function setErrorPage(statusCode: number, fatal = false): void {
state.viewsState.error = new ErrorPageState(statusCode, fatal, true);
}
function removeErrorPage(): void {
state.viewsState.error.visible = false;
}
function clear(): void {
state.viewsState.activeModal = null;
state.viewsState.isSuccessfulPasswordResetShown = false;
@ -170,6 +187,7 @@ export const useAppStore = defineStore('app', () => {
state.viewsState.selectedPricingPlan = null;
state.viewsState.hasShownPricingPlan = false;
state.viewsState.activeDropdown = '';
state.viewsState.error.visible = false;
}
return {
@ -177,8 +195,9 @@ export const useAppStore = defineStore('app', () => {
getConfig,
toggleActiveDropdown,
toggleSuccessfulPasswordReset,
updateActiveModal,
removeActiveModal,
toggleHasJustLoggenIn,
toggleHasJustLoggedIn: toggleHasJustLoggenIn,
changeState,
setSatelliteName,
setPartneredSatellites,
@ -194,7 +213,8 @@ export const useAppStore = defineStore('app', () => {
setManagePassphraseStep,
setHasShownPricingPlan,
closeDropdowns,
updateActiveModal,
setErrorPage,
removeErrorPage,
clear,
};
});

View File

@ -0,0 +1,162 @@
// Copyright (C) 2023 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<div class="error-area">
<div class="error-area__logo-wrapper">
<Logo class="error-area__logo-wrapper__logo" @click="goToHomepage" />
</div>
<div class="error-area__container">
<h1 class="error-area__container__title">{{ statusCode }}</h1>
<h2 class="error-area__container__subtitle">{{ message }}</h2>
<VButton
class="error-area__container__button"
:label="'Go ' + (isFatal ? 'to homepage' : 'back')"
width="auto"
height="auto"
border-radius="26px"
:on-press="onButtonClick"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted } from 'vue';
import { useRouter } from '@/utils/hooks';
import { useAppStore } from '@/store/modules/appStore';
import { MetaUtils } from '@/utils/meta';
import VButton from '@/components/common/VButton.vue';
import Logo from '@/../static/images/logo.svg';
const router = useRouter();
const appStore = useAppStore();
const messages = new Map<number, string>([
[404, 'Oops, page not found.'],
[500, 'Internal server error.'],
]);
/**
* Retrieves the error's status code from the store.
*/
const statusCode = computed((): number => {
return appStore.state.viewsState.error.statusCode;
});
/**
* Retrieves the message corresponding to the error's status code.
*/
const message = computed((): string => {
return messages.get(statusCode.value) || 'An error occurred.';
});
/**
* Indicates whether the error is unrecoverable.
*/
const isFatal = computed((): boolean => {
return appStore.state.viewsState.error.fatal;
});
/**
* Navigates to the homepage.
*/
function goToHomepage(): void {
window.location.href = MetaUtils.getMetaContent('homepage-url');
}
/**
* Navigates to the homepage if fatal or the previous route otherwise.
*/
function onButtonClick(): void {
if (isFatal.value) {
goToHomepage();
return;
}
router.back();
}
/**
* Lifecycle hook after initial render. Sets page title.
*/
onMounted(() => {
const satName = appStore.state.satelliteName;
document.title = statusCode.value.toString() + (satName ? ' | ' + satName : '');
});
</script>
<style scoped lang="scss">
.error-area {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
padding: 52px 24px;
box-sizing: border-box;
display: flex;
background: url('~@/../static/images/errors/dotWorld.png') no-repeat center 178px;
flex-direction: column;
justify-content: center;
overflow-y: auto;
&__logo-wrapper {
height: 30.89px;
display: flex;
justify-content: center;
position: absolute;
top: 52px;
left: 0;
right: 0;
margin-bottom: 52px;
&__logo {
height: 100%;
width: auto;
cursor: pointer;
}
}
&__container {
display: flex;
flex-direction: column;
align-items: center;
font-family: 'font_bold', sans-serif;
text-align: center;
&__title {
font-size: 90px;
line-height: 90px;
}
&__subtitle {
margin-top: 20px;
font-size: 28px;
}
&__button {
margin-top: 32px;
padding: 16px 37.5px;
:deep(.label) {
font-family: 'font_bold', sans-serif;
line-height: 20px;
}
}
}
}
@media screen and (max-height: 500px) {
.error-area {
justify-content: flex-start;
&__logo-wrapper {
position: unset;
}
}
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB