add loading screens instead of dropping the user into an app that's not ready to use

This commit is contained in:
liquidex 2024-08-24 20:09:52 +02:00
parent 7f78d0ce1b
commit 84abba3e0b
6 changed files with 147 additions and 27 deletions

View file

@ -30,7 +30,7 @@ export class BrushEditor extends HTMLElement {
}),
);
});
this.#resizeTextArea();
requestAnimationFrame(() => this.#resizeTextArea());
this.errorHeader = this.appendChild(document.createElement("h1"));
this.errorHeader.classList.add("error-header");

View file

@ -0,0 +1,19 @@
export class ConnectionStatus extends HTMLElement {
connectedCallback() {
this.loggingInDialog = this.querySelector("dialog[name='logging-in-dialog']");
this.loggingInThrobber = this.loggingInDialog.querySelector("rkgk-throbber");
// This is a progress dialog and shouldn't be closed.
this.loggingInDialog.addEventListener("cancel", (event) => event.preventDefault());
}
showLoggingIn() {
this.loggingInDialog.showModal();
}
hideLoggingIn() {
this.loggingInDialog.close();
}
}
customElements.define("rkgk-connection-status", ConnectionStatus);

View file

@ -4,6 +4,8 @@
--color-text: #111;
--color-error: #db344b;
--color-brand-blue: #40b1f4;
--color-panel-border: rgba(0, 0, 0, 20%);
--color-panel-background: #fff;
@ -71,6 +73,14 @@ main {
height: 100%;
position: relative;
&>.fullscreen {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
&>rkgk-canvas-renderer {
width: 100%;
height: 100%;
@ -95,6 +105,14 @@ main {
top: 0;
margin: 16px;
}
&>#js-loading {
background-color: var(--color-panel-background);
display: flex;
align-items: center;
justify-content: center;
}
}
/* Buttons */
@ -133,14 +151,33 @@ dialog::backdrop {
/* Throbbers */
@keyframes rkgk-throbber-loading {
0% {
clip-path: inset(0% 100% 0% 0%);
animation-timing-function: cubic-bezier(0.12, 0, 0.39, 0);
}
50% {
clip-path: inset(0% 0% 0% 0%);
animation-timing-function: cubic-bezier(0.61, 1, 0.88, 1);
}
100% {
clip-path: inset(0% 0% 0% 100%);
}
}
rkgk-throbber {
display: inline;
&.loading {
&::before {
/* This could use an entertaining animation. */
content: "Please wait...";
}
display: block;
width: 16px;
height: 16px;
background-color: var(--color-brand-blue);
animation: infinite alternate rkgk-throbber-loading;
/* I wonder how many people will get _that_ reference. */
animation-duration: calc(60s / 141.98);
}
&.error {
@ -254,3 +291,17 @@ rkgk-welcome {
}
}
}
/* Connection status dialogs */
rkgk-connection-status {
&>dialog[name='logging-in-dialog'][open] {
border: none;
outline: none;
background: none;
display: flex;
gap: 8px;
align-items: center;
}
}

View file

@ -2,6 +2,8 @@
<html>
<head>
<meta charset="UTF-8">
<title>rakugaki</title>
<link rel="stylesheet" href="static/index.css">
@ -10,6 +12,7 @@
<script src="static/brush-editor.js" type="module"></script>
<script src="static/canvas-renderer.js" type="module"></script>
<script src="static/connection-status.js" type="module"></script>
<script src="static/framework.js" type="module"></script>
<script src="static/reticle-renderer.js" type="module"></script>
<script src="static/session.js" type="module"></script>
@ -17,13 +20,13 @@
<script src="static/viewport.js" type="module"></script>
<script src="static/welcome.js" type="module"></script>
<script src="static/index.js" type="module"></script>
<script src="static/index.js" type="module" defer></script>
</head>
<body>
<main>
<rkgk-canvas-renderer></rkgk-canvas-renderer>
<rkgk-reticle-renderer></rkgk-reticle-renderer>
<rkgk-canvas-renderer class="fullscreen"></rkgk-canvas-renderer>
<rkgk-reticle-renderer class="fullscreen"></rkgk-reticle-renderer>
<rkgk-brush-editor></rkgk-brush-editor>
<rkgk-welcome>
@ -49,6 +52,25 @@
</form>
</dialog>
</rkgk-welcome>
<rkgk-connection-status>
<dialog name="logging-in-dialog">
<rkgk-throbber class="loading"></rkgk-throbber><p>Logging in...</p>
</dialog>
</rkgk-connection-status>
<div class="fullscreen" id="js-loading">
<rkgk-throbber class="loading"></rkgk-throbber>
<noscript>
<style>
#js-loading>rkgk-throbber { display: none; }
</style>
<p>
rakugaki is a web app and does not work without JavaScript :(<br>
but I swear it's a very lightweight and delightful web app!
</p>
</noscript>
</div>
</main>
</body>
</html>

View file

@ -1,5 +1,12 @@
import { Wall } from "./wall.js";
import { getLoginSecret, getUserId, newSession, waitForLogin } from "./session.js";
import {
getLoginSecret,
getUserId,
isUserLoggedIn,
newSession,
registerUser,
waitForLogin,
} from "./session.js";
import { debounce } from "./framework.js";
import { ReticleCursor } from "./reticle-renderer.js";
@ -9,7 +16,10 @@ let main = document.querySelector("main");
let canvasRenderer = main.querySelector("rkgk-canvas-renderer");
let reticleRenderer = main.querySelector("rkgk-reticle-renderer");
let brushEditor = main.querySelector("rkgk-brush-editor");
let welcome = main.querySelector("rkgk-welcome");
let connectionStatus = main.querySelector("rkgk-connection-status");
document.getElementById("js-loading").remove();
reticleRenderer.connectViewport(canvasRenderer.viewport);
function updateUrl(session, viewport) {
@ -49,6 +59,16 @@ function readUrl() {
// In the background, connect to the server.
(async () => {
console.info("checking for user registration status");
if (!isUserLoggedIn()) {
await welcome.show({
async onRegister(nickname) {
return await registerUser(nickname);
},
});
}
connectionStatus.showLoggingIn();
await waitForLogin();
console.info("login ready! starting session");
@ -67,6 +87,8 @@ function readUrl() {
);
localStorage.setItem("rkgk.mostRecentWallId", session.wallId);
connectionStatus.hideLoggingIn();
updateUrl(session, canvasRenderer.viewport);
let wall = new Wall(session.wallInfo);

View file

@ -1,35 +1,41 @@
import { isUserLoggedIn, registerUser } from "./session.js";
export class Welcome extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.dialog = this.querySelector("dialog[name='welcome-dialog']");
this.form = this.dialog.querySelector("form");
this.welcomeDialog = this.querySelector("dialog[name='welcome-dialog']");
this.welcomeForm = this.welcomeDialog.querySelector("form");
this.nicknameField = this.querySelector("input[name='nickname']");
this.registerButton = this.querySelector("button[name='register']");
this.registerProgress = this.querySelector("rkgk-throbber[name='register-progress']");
if (!isUserLoggedIn()) {
this.dialog.showModal();
// Once the dialog is open, you need an account to use the website.
this.welcomeDialog.addEventListener("cancel", (event) => event.preventDefault());
}
// Require an account to use the website.
this.dialog.addEventListener("close", (event) => event.preventDefault());
show({ onRegister }) {
let resolvePromise;
let promise = new Promise((resolve) => (resolvePromise = resolve));
this.form.addEventListener("submit", async (event) => {
event.preventDefault();
this.welcomeDialog.showModal();
this.registerProgress.beginLoading();
let response = await registerUser(this.nicknameField.value);
if (response.status != "ok") {
this.registerProgress.showError(response.message);
}
let submitListener = async (event) => {
event.preventDefault();
this.dialog.close();
});
}
this.registerProgress.beginLoading();
let response = await onRegister(this.nicknameField.value);
if (response.status == "ok") {
this.welcomeDialog.close();
resolvePromise();
} else {
this.registerProgress.showError(response.message);
}
this.welcomeForm.removeEventListener("submit", submitListener);
};
this.welcomeForm.addEventListener("submit", submitListener);
return promise;
}
}