impr: loading page improvements (@miodec) (#6893)

- Refactor the loading page and the functions responsible for showing
elements
- Navigate loading options no longer override but they are used BEFORE
the page loading options. Keyframes are scaled accordingly to transition
smoothly
 - Removed the error element from the account page
 - Added a rejection / error handler to the loading page
 - Removed one more dependency from the account controller
This commit is contained in:
Jack 2025-08-19 21:10:51 +02:00 committed by GitHub
parent b6959552ab
commit 725fde1ae1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 197 additions and 168 deletions

View file

@ -1,10 +1,4 @@
<div class="page pageAccount hidden full-width" id="pageAccount">
<div class="error hidden content-grid">
<div class="icon">
<i class="fas fa-fw fa-times"></i>
</div>
<div class="text">Error</div>
</div>
<div class="content full-width content-grid">
<div class="profile">
<div class="details both">

View file

@ -1,13 +1,12 @@
<div class="page pageLoading" id="pageLoading">
<div class="preloader">
<div class="icon">
<i class="fas fa-fw fa-spin fa-circle-notch"></i>
</div>
<div class="barWrapper hidden">
<div class="bar">
<div class="fill"></div>
</div>
<div class="text"></div>
</div>
<div class="spinner">
<i class="fas fa-fw fa-spin fa-circle-notch"></i>
</div>
<div class="error hidden">
<i class="fas fa-fw fa-times"></i>
</div>
<div class="bar hidden">
<div class="fill"></div>
</div>
<div class="text hidden">Loading...</div>
</div>

View file

@ -1,21 +1,6 @@
.pageAccount {
height: 100%;
.error {
display: grid;
place-items: center;
align-content: center;
height: 100%;
.icon {
font-size: 2rem;
color: var(--error-color);
}
.text {
font-size: 1rem;
text-align: center;
}
}
.accountVerificatinNotice {
background: var(--bg-color);
border-radius: var(--roundness);

View file

@ -1,38 +1,40 @@
.pageLoading {
.preloader {
text-align: center;
text-align: center;
place-self: center;
align-content: center;
display: grid;
gap: 1rem;
width: 100%;
.spinner,
.error {
font-size: 2rem;
}
.spinner {
color: var(--main-color);
}
.error {
color: var(--error-color);
}
.text {
height: 1.25em;
}
.bar {
max-width: 20rem;
width: 100%;
height: 0.5rem;
background: var(--sub-alt-color);
border-radius: var(--roundness);
justify-self: center;
display: grid;
.barWrapper {
justify-content: center;
display: grid;
gap: 1rem;
grid-row: 1;
grid-column: 1;
.text {
height: 1.25em;
}
.bar {
width: 20rem;
height: 0.5rem;
background: var(--sub-alt-color);
border-radius: var(--roundness);
justify-self: center;
.fill {
height: 100%;
width: 0%;
background: var(--main-color);
border-radius: var(--roundness);
// transition: 1s;
}
}
}
.icon {
grid-row: 1;
grid-column: 1;
font-size: 2rem;
color: var(--main-color);
margin-bottom: 1rem;
.fill {
height: 100%;
width: 50%;
background: var(--main-color);
border-radius: var(--roundness);
}
}
}

View file

@ -6,7 +6,6 @@ import * as DB from "../db";
import * as Loader from "../elements/loader";
import * as LoginPage from "../pages/login";
import * as RegisterCaptchaModal from "../modals/register-captcha";
import * as Account from "../pages/account";
import {
GoogleAuthProvider,
GithubAuthProvider,
@ -97,9 +96,6 @@ async function getDataAndInit(): Promise<boolean> {
fb.functions.applyGlobalCSS();
}
}
if (window.location.pathname === "/account") {
await Account.downloadResults();
}
return true;
} catch (error) {
console.error(error);
@ -170,28 +166,10 @@ export async function onAuthStateChanged(
},
];
if (
window.location.pathname === "/account" ||
window.location.pathname === "/login"
) {
keyframes = [
{
percentage: 40,
durationMs: 1000,
text: "Downloading user data...",
},
{
percentage: 90,
durationMs: 1000,
text: "Downloading results...",
},
];
}
//undefined means navigate to whatever the current window.location.pathname is
await navigate(undefined, {
force: true,
overrideLoadingOptions: {
loadingOptions: {
shouldLoad: () => {
return user !== null;
},

View file

@ -21,7 +21,7 @@ type ChangeOptions = {
force?: boolean;
params?: Record<string, string>;
data?: unknown;
overrideLoadingOptions?: LoadingOptions;
loadingOptions?: LoadingOptions;
};
function updateOpenGraphUrl(): void {
@ -50,11 +50,77 @@ function updateTitle(nextPage: { id: string; display?: string }): void {
}
}
async function showLoading({
loadingOptions,
totalDuration,
easingMethod,
}: {
loadingOptions: LoadingOptions[];
totalDuration: number;
easingMethod: Misc.JQueryEasing;
}): Promise<void> {
PageLoading.page.element.removeClass("hidden").css("opacity", 0);
await PageLoading.page.beforeShow({});
const fillDivider = loadingOptions.length;
const fillOffset = 100 / fillDivider;
//void here to run the loading promise as soon as possible
void Misc.promiseAnimation(
PageLoading.page.element,
{
opacity: "1",
},
totalDuration / 2,
easingMethod
);
for (let i = 0; i < loadingOptions.length; i++) {
const currentOffset = fillOffset * i;
const options = loadingOptions[i] as LoadingOptions;
if (options.style === "bar") {
await PageLoading.showBar();
if (i === 0) {
await PageLoading.updateBar(0, 0);
PageLoading.updateText("");
}
} else {
PageLoading.showSpinner();
}
if (options.style === "bar") {
await getLoadingPromiseWithBarKeyframes(
options,
fillDivider,
currentOffset
);
void PageLoading.updateBar(100, 125);
PageLoading.updateText("Done");
} else {
await options.waitFor();
}
}
await Misc.promiseAnimation(
PageLoading.page.element,
{
opacity: "0",
},
totalDuration / 2,
easingMethod
);
await PageLoading.page.afterHide();
PageLoading.page.element.addClass("hidden");
}
async function getLoadingPromiseWithBarKeyframes(
loadingOptions: Extract<
NonNullable<Page<unknown>["loadingOptions"]>,
{ style: "bar" }
>
>,
fillDivider: number,
fillOffset: number
): Promise<void> {
let aborted = false;
let loadingPromise = loadingOptions.waitFor();
@ -66,7 +132,10 @@ async function getLoadingPromiseWithBarKeyframes(
if (keyframe.text !== undefined) {
PageLoading.updateText(keyframe.text);
}
await PageLoading.updateBar(keyframe.percentage, keyframe.durationMs);
await PageLoading.updateBar(
fillOffset + keyframe.percentage / fillDivider,
keyframe.durationMs
);
}
})();
@ -145,58 +214,47 @@ export async function change(
previousPage.element.addClass("hidden");
await previousPage?.afterHide();
//show loading page if needed
try {
let loadingOptions: LoadingOptions[] = [];
if (options.loadingOptions) {
loadingOptions.push(options.loadingOptions);
}
if (nextPage.loadingOptions) {
loadingOptions.push(nextPage.loadingOptions);
}
if (loadingOptions.length > 0) {
const shouldShowLoading =
options.loadingOptions?.shouldLoad() ||
nextPage.loadingOptions?.shouldLoad();
if (shouldShowLoading === true) {
await showLoading({
loadingOptions,
totalDuration,
easingMethod,
});
}
}
} catch (error) {
pages.loading.element.addClass("active");
ActivePage.set(pages.loading.id);
Focus.set(false);
PageLoading.showError();
PageLoading.updateText(
`Failed to load the ${nextPage.id} page: ${
error instanceof Error ? error.message : String(error)
}`
);
PageTransition.set(false);
return false;
}
//between
updateTitle(nextPage);
ActivePage.set(nextPage.id);
updateOpenGraphUrl();
const loadingOptions =
options.overrideLoadingOptions ?? nextPage.loadingOptions;
//show loading page if needed
if (loadingOptions && loadingOptions.shouldLoad()) {
pages.loading.element.removeClass("hidden").css("opacity", 0);
await pages.loading.beforeShow({});
if (loadingOptions.style === "bar") {
await PageLoading.showBar();
await PageLoading.updateBar(0, 0);
PageLoading.updateText("");
} else {
PageLoading.showSpinner();
}
//void here to run the loading promise as soon as possible
void Misc.promiseAnimation(
pages.loading.element,
{
opacity: "1",
},
totalDuration / 2,
easingMethod
);
if (loadingOptions.style === "bar") {
await getLoadingPromiseWithBarKeyframes(loadingOptions);
void PageLoading.updateBar(100, 125);
PageLoading.updateText("Done");
} else {
await loadingOptions.waitFor();
}
await Misc.promiseAnimation(
pages.loading.element,
{
opacity: "0",
},
totalDuration / 2,
easingMethod
);
await pages.loading.afterHide();
pages.loading.element.addClass("hidden");
}
Focus.set(false);
//next page

View file

@ -15,7 +15,7 @@ type NavigateOptions = {
force?: boolean;
empty?: boolean;
data?: unknown;
overrideLoadingOptions?: LoadingOptions;
loadingOptions?: LoadingOptions;
};
function pathToRegex(path: string): RegExp {

View file

@ -987,26 +987,14 @@ export async function downloadResults(offset?: number): Promise<void> {
}
}
function showError(message: string): void {
$(".pageAccount .error .text").html(message);
$(".pageAccount .error").removeClass("hidden");
$(".pageAccount .content").remove();
}
async function update(): Promise<void> {
if (DB.getSnapshot() === null) {
showError(
"Looks like your account data didn't download correctly. Please refresh the page.<br>If this error persists, please contact support."
);
} else {
await downloadResults();
try {
await Misc.sleep(0);
await fillContent();
} catch (e) {
console.error(e);
Notifications.add(`Something went wrong: ${e}`, -1);
}
await downloadResults();
try {
await Misc.sleep(0);
await fillContent();
} catch (e) {
console.error(e);
Notifications.add(`Something went wrong: ${e}`, -1);
}
}
@ -1344,12 +1332,19 @@ export const page = new Page({
shouldLoad: () => {
return DB.getSnapshot()?.results === undefined;
},
waitFor: downloadResults,
waitFor: async () => {
if (DB.getSnapshot() === null) {
throw new Error(
"Looks like your account data didn't download correctly. Please refresh the page.<br>If this error persists, please contact support."
);
}
return downloadResults();
},
style: "bar",
keyframes: [
{
percentage: 100,
durationMs: 3000,
percentage: 90,
durationMs: 2000,
text: "Downloading results...",
},
],

View file

@ -1,12 +1,19 @@
import Page from "./page";
import * as Skeleton from "../utils/skeleton";
const pageEl = $(".page.pageLoading");
const barEl = pageEl.find(".bar");
const errorEl = pageEl.find(".error");
const spinnerEl = pageEl.find(".spinner");
const textEl = pageEl.find(".text");
export async function updateBar(
percentage: number,
duration: number
): Promise<void> {
return new Promise((resolve) => {
$(".pageLoading .fill")
barEl
.find(".fill")
.stop(true, false)
.animate(
{
@ -21,22 +28,33 @@ export async function updateBar(
}
export function updateText(text: string): void {
$(".pageLoading .text").text(text);
textEl.removeClass("hidden").html(text);
}
export function showSpinner(): void {
$(".pageLoading .preloader .icon").removeClass("hidden");
$(".pageLoading .preloader .barWrapper").addClass("hidden");
barEl.addClass("hidden");
errorEl.addClass("hidden");
spinnerEl.removeClass("hidden");
textEl.addClass("hidden");
}
export function showError(): void {
barEl.addClass("hidden");
spinnerEl.addClass("hidden");
errorEl.removeClass("hidden");
textEl.addClass("hidden");
}
export async function showBar(): Promise<void> {
$(".pageLoading .preloader .icon").addClass("hidden");
$(".pageLoading .preloader .barWrapper").removeClass("hidden");
barEl.removeClass("hidden");
errorEl.addClass("hidden");
spinnerEl.addClass("hidden");
textEl.addClass("hidden");
}
export const page = new Page({
id: "loading",
element: $(".page.pageLoading"),
element: pageEl,
path: "/",
afterHide: async (): Promise<void> => {
Skeleton.remove("pageLoading");