SignIn.tsx
| 1 | import { type Accessor, createEffect, type Setter } from "solid-js"; |
| 2 | import { ClearSVG } from "./SVG"; |
| 3 | import "./SignIn.css"; |
| 4 | import { toast } from "../utils"; |
| 5 | import { LoadingSpinner } from "./LoadingScreen"; |
| 6 | |
| 7 | interface SignInProps { |
| 8 | showSignIn: Accessor<boolean>; |
| 9 | setShowSignIn: Setter<boolean>; |
| 10 | onLoggedIn: () => void; |
| 11 | } |
| 12 | |
| 13 | export function SignIn(props: SignInProps) { |
| 14 | let dialogElement!: HTMLDialogElement; |
| 15 | let formElement!: HTMLFormElement; |
| 16 | let usernameInput!: HTMLInputElement; |
| 17 | let passwordInput!: HTMLInputElement; |
| 18 | let loadingElement!: HTMLDivElement; |
| 19 | let submitButton!: HTMLButtonElement; |
| 20 | |
| 21 | createEffect(() => { |
| 22 | if (props.showSignIn()) { |
| 23 | dialogElement.showModal(); |
| 24 | } |
| 25 | }); |
| 26 | |
| 27 | function handleClose() { |
| 28 | dialogElement.close(); |
| 29 | usernameInput.value = ""; |
| 30 | passwordInput.value = ""; |
| 31 | props.setShowSignIn(false); |
| 32 | } |
| 33 | |
| 34 | async function doSignIn(e: Event) { |
| 35 | e.preventDefault(); |
| 36 | usernameInput.setCustomValidity(""); |
| 37 | if (!formElement.reportValidity()) return; |
| 38 | if (usernameInput.value.includes(":")) { |
| 39 | usernameInput.setCustomValidity('Username can\'t contain a colon ":"'); |
| 40 | usernameInput.reportValidity(); |
| 41 | return; |
| 42 | } |
| 43 | loadingElement.style.display = "block"; |
| 44 | submitButton.disabled = true; |
| 45 | try { |
| 46 | const response = await fetch("/login", { |
| 47 | method: "POST", |
| 48 | body: `${usernameInput.value}:${passwordInput.value}`, |
| 49 | }); |
| 50 | if (response.status === 200) { |
| 51 | toast("Login successful", "success"); |
| 52 | props.onLoggedIn(); |
| 53 | handleClose(); |
| 54 | } else if (response.status === 401) { |
| 55 | toast("Login failed: wrong credentials", "error"); |
| 56 | } else { |
| 57 | toast(`Login failed: unknown error, code: ${response.status}`, "error", await (await response.blob()).text()); |
| 58 | } |
| 59 | } finally { |
| 60 | loadingElement.style.display = "none"; |
| 61 | submitButton.disabled = false; |
| 62 | } |
| 63 | } |
| 64 | |
| 65 | return ( |
| 66 | <dialog ref={dialogElement} onClose={handleClose} onclick={(e) => e.target.nodeName === "DIALOG" && handleClose()}> |
| 67 | <form ref={formElement} onSubmit={doSignIn}> |
| 68 | <div class="signin-header title"> |
| 69 | Sign in |
| 70 | <button type="button" title="Close dialog" onClick={handleClose}> |
| 71 | <ClearSVG /> |
| 72 | </button> |
| 73 | </div> |
| 74 | <hr /> |
| 75 | <label for="username-input">Username:</label> |
| 76 | <input |
| 77 | ref={usernameInput} |
| 78 | id="username-input" |
| 79 | type="text" |
| 80 | minlength={1} |
| 81 | required |
| 82 | value="" |
| 83 | onChange={() => {}} |
| 84 | /> |
| 85 | <label for="password-input">Password:</label> |
| 86 | <input |
| 87 | ref={passwordInput} |
| 88 | id="password-input" |
| 89 | type="password" |
| 90 | minlength={1} |
| 91 | required |
| 92 | value="" |
| 93 | onChange={() => {}} |
| 94 | /> |
| 95 | <button ref={submitButton} type="submit" class="signin-button" title="Submit sign in"> |
| 96 | Submit |
| 97 | <div ref={loadingElement} style={{ display: "none", width: "24px", height: "24px" }}> |
| 98 | <LoadingSpinner center /> |
| 99 | </div> |
| 100 | </button> |
| 101 | </form> |
| 102 | </dialog> |
| 103 | ); |
| 104 | } |
| 105 |