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 = this.appendChild(document.createElement("h1"));
this.errorHeader.classList.add("error-header"); 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-text: #111;
--color-error: #db344b; --color-error: #db344b;
--color-brand-blue: #40b1f4;
--color-panel-border: rgba(0, 0, 0, 20%); --color-panel-border: rgba(0, 0, 0, 20%);
--color-panel-background: #fff; --color-panel-background: #fff;
@ -71,6 +73,14 @@ main {
height: 100%; height: 100%;
position: relative; position: relative;
&>.fullscreen {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
&>rkgk-canvas-renderer { &>rkgk-canvas-renderer {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -95,6 +105,14 @@ main {
top: 0; top: 0;
margin: 16px; margin: 16px;
} }
&>#js-loading {
background-color: var(--color-panel-background);
display: flex;
align-items: center;
justify-content: center;
}
} }
/* Buttons */ /* Buttons */
@ -133,14 +151,33 @@ dialog::backdrop {
/* Throbbers */ /* 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 { rkgk-throbber {
display: inline; display: inline;
&.loading { &.loading {
&::before { display: block;
/* This could use an entertaining animation. */ width: 16px;
content: "Please wait..."; 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 { &.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> <html>
<head> <head>
<meta charset="UTF-8">
<title>rakugaki</title> <title>rakugaki</title>
<link rel="stylesheet" href="static/index.css"> <link rel="stylesheet" href="static/index.css">
@ -10,6 +12,7 @@
<script src="static/brush-editor.js" type="module"></script> <script src="static/brush-editor.js" type="module"></script>
<script src="static/canvas-renderer.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/framework.js" type="module"></script>
<script src="static/reticle-renderer.js" type="module"></script> <script src="static/reticle-renderer.js" type="module"></script>
<script src="static/session.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/viewport.js" type="module"></script>
<script src="static/welcome.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> </head>
<body> <body>
<main> <main>
<rkgk-canvas-renderer></rkgk-canvas-renderer> <rkgk-canvas-renderer class="fullscreen"></rkgk-canvas-renderer>
<rkgk-reticle-renderer></rkgk-reticle-renderer> <rkgk-reticle-renderer class="fullscreen"></rkgk-reticle-renderer>
<rkgk-brush-editor></rkgk-brush-editor> <rkgk-brush-editor></rkgk-brush-editor>
<rkgk-welcome> <rkgk-welcome>
@ -49,6 +52,25 @@
</form> </form>
</dialog> </dialog>
</rkgk-welcome> </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> </main>
</body> </body>
</html> </html>

View file

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

View file

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