Breaking apart some large components

This commit is contained in:
Imbus 2023-11-15 14:21:14 +01:00
parent 6b81763259
commit 9f3685caed
9 changed files with 165 additions and 145 deletions

View file

@ -34,4 +34,4 @@
"vite-plugin-qrcode": "^0.2.2",
"vite-plugin-solid": "^2.7.2"
}
}
}

View file

@ -10,8 +10,8 @@ import {
// So far we only have one modal, but we can add more later
// by adding more fields to this interface, or maybe an enum
interface ModalContextType {
loginModalOpen: Accessor<boolean>;
setLoginModalOpen: (value: boolean) => void;
isOpen: Accessor<boolean>;
setOpen: (value: boolean) => void;
}
interface LoginContextType {
@ -21,6 +21,7 @@ interface LoginContextType {
setUsername: (value: string) => void;
loggedIn: () => boolean;
logOut: () => void;
logIn: (username: string, token: string) => void;
}
// It is unclear to me if this is the idiomatic way to do this in Solid
@ -46,6 +47,13 @@ export function GlobalStateProvider(props: {
return token() != "" && username() != "";
}
function logIn (username: string, token: string): void {
setUsername(username);
setToken(token);
localStorage.setItem("token", token);
localStorage.setItem("username", username);
}
function logOut(): void {
localStorage.removeItem("token");
localStorage.removeItem("username");
@ -55,9 +63,9 @@ export function GlobalStateProvider(props: {
return (
<>
<ModalContext.Provider value={{ loginModalOpen, setLoginModalOpen }}>
<ModalContext.Provider value={{ isOpen: loginModalOpen, setOpen: setLoginModalOpen }}>
<LoginContext.Provider
value={{ token, setToken, username, setUsername, loggedIn, logOut }}
value={{ token, setToken, username, setUsername, loggedIn, logOut, logIn }}
>
{props.children}
</LoginContext.Provider>

View file

@ -0,0 +1,23 @@
import { JSXElement, Show, useContext } from "solid-js";
import { LoginContext, ModalContext } from "./GlobalState";
import { UserCircle } from "./Icons";
export function LoginButton(): JSXElement {
const modal_ctx = useContext(ModalContext)!;
const login_ctx = useContext(LoginContext)!;
const clickHandler = (): void => {
if (login_ctx.loggedIn()) login_ctx.logOut();
else modal_ctx.setOpen(true);
};
return (
<div class="btn btn-ghost text-sm capitalize" onClick={clickHandler}>
<Show when={login_ctx.loggedIn()} fallback="Login">
{login_ctx.username()}
</Show>
<UserCircle />
</div>
);
}

View file

@ -8,16 +8,16 @@ export function LoginModal(): JSXElement {
const modal_ctx = useContext(ModalContext)!;
const closeModal = (): void => {
modal_ctx.setLoginModalOpen(false);
modal_ctx.setOpen(false);
};
// Close the modal when the component is unmounted
onCleanup(() => {
modal_ctx.setLoginModalOpen(false);
modal_ctx.setOpen(false);
});
return (
<Show when={modal_ctx.loginModalOpen()}>
<Show when={modal_ctx.isOpen()}>
<dialog class="modal modal-open">
<div class="modal-box">
<form method="dialog">

36
client-solid/src/Menu.tsx Normal file
View file

@ -0,0 +1,36 @@
import { A } from "@solidjs/router";
import { JSXElement, Show, useContext } from "solid-js";
import { LoginContext } from "./GlobalState";
import { Home, Plus } from "./Icons";
// Represents a single list item in the menu bar
export function MenuItem(props: {
href: string;
children: JSXElement;
}): JSXElement {
return (
<li>
<A class="justify-center" href={props.href} end>
{props.children}
</A>
</li>
);
}
// Represents the menu bar at the top of the page
export function Menu(): JSXElement {
const login_ctx = useContext(LoginContext)!;
return (
<Show when={login_ctx.loggedIn()}>
<ul class="menu space-y-2 rounded-box md:menu-horizontal md:space-x-2 md:space-y-0">
<MenuItem href="/">
<Home />
</MenuItem>
<MenuItem href="/new">
<Plus />
</MenuItem>
</ul>
</Show>
);
}

View file

@ -1,46 +1,11 @@
import { A } from "@solidjs/router";
import { JSXElement, Show, useContext } from "solid-js";
import { JSXElement } from "solid-js";
import { LoginContext, ModalContext } from "./GlobalState";
import { Flake, Home, Plus, UserCircle } from "./Icons";
// Represents a single list item in the menu bar
function MenuItem(props: { href: string; children: JSXElement }): JSXElement {
return (
<li>
<A class="justify-center" href={props.href} end>
{props.children}
</A>
</li>
);
}
// Represents the menu bar at the top of the page
function Menu(): JSXElement {
const login_ctx = useContext(LoginContext)!;
return (
<Show when={login_ctx.loggedIn()}>
<ul class="menu space-y-2 rounded-box md:menu-horizontal md:space-x-2 md:space-y-0">
<MenuItem href="/">
<Home />
</MenuItem>
<MenuItem href="/new">
<Plus />
</MenuItem>
</ul>
</Show>
);
}
import { Flake } from "./Icons";
import { LoginButton } from "./LoginButton";
import { Menu } from "./Menu";
export function Navbar(): JSXElement {
const modal_ctx = useContext(ModalContext)!;
const login_ctx = useContext(LoginContext)!;
const clickHandler = (): void => {
if (login_ctx.loggedIn()) login_ctx.logOut();
else modal_ctx.setLoginModalOpen(true);
};
return (
<div class="max-w navbar max-w-3xl rounded-box text-neutral-content md:my-4">
<div class="flex-1">
@ -53,18 +18,7 @@ export function Navbar(): JSXElement {
<Menu />
</div>
<div class="flex flex-1 justify-end">
<A
href="#"
class="btn btn-ghost text-sm capitalize"
onClick={clickHandler}
>
{
<Show when={login_ctx.loggedIn()} fallback="Login">
{login_ctx.username()}
</Show>
}
<UserCircle />
</A>
<LoginButton />
</div>
</div>
);

View file

@ -1,10 +1,11 @@
import { JSXElement, createSignal, useContext } from "solid-js";
import { JSXElement, Show, createSignal, useContext } from "solid-js";
import { LoginContext, ModalContext } from "../GlobalState";
import { AuthResponse, submitLogin } from "../api";
export function LoginForm(): JSXElement {
const modal_ctx = useContext(ModalContext);
const login_ctx = useContext(LoginContext);
const modal_ctx = useContext(ModalContext)!;
const login_ctx = useContext(LoginContext)!;
const [username, setUsername] = createSignal<string>("");
const [password, setPassword] = createSignal<string>("");
const [waiting, setWaiting] = createSignal(false);
@ -18,22 +19,25 @@ export function LoginForm(): JSXElement {
}, 1000);
}
const clearFields = (): void => {
setUsername("");
setPassword("");
};
const success = (response: AuthResponse): void => {
setWaiting(false);
setError(false);
clearFields();
login_ctx.logIn(response.username, response.token);
modal_ctx.setOpen(false);
};
async function loginPress(e: Event): Promise<void> {
e.preventDefault();
setWaiting(true);
submitLogin(username(), password()).then((token): void => {
if (token != "") {
setWaiting(false);
setError(false);
login_ctx?.setUsername(username());
login_ctx?.setToken(token);
modal_ctx?.setLoginModalOpen(false);
setUsername("");
setPassword("");
} else {
loginFailed();
}
});
const data = await submitLogin(username(), password());
if (data) success(data as AuthResponse);
else loginFailed();
}
return (
@ -59,35 +63,13 @@ export function LoginForm(): JSXElement {
/>
<button
class={"btn btn-primary" + (error() ? " btn-error" : "")}
classList={{ "btn btn-primary": !error(), "btn btn-error": error() }}
onClick={loginPress}
>
{waiting() ? "Logging in..." : "Login"}
<Show when={waiting()} fallback="Login">
Logging in...
</Show>
</button>
</form>
);
}
// This function is responsible for sending the login request to the server
// and storing the token in localstorage
export async function submitLogin(
username: string,
password: string
): Promise<string> {
if (username == "" || password == "") return "";
const response = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (response.ok) {
const data = await response.json();
if (data.token && data.username) {
localStorage.setItem("token", data.token);
localStorage.setItem("username", data.username);
return data.token;
}
}
return "";
}

View file

@ -1,10 +1,11 @@
import { JSXElement, createSignal, useContext } from "solid-js";
import { JSXElement, Show, createSignal, useContext } from "solid-js";
import { LoginContext, ModalContext } from "../GlobalState";
import { AuthResponse, submitRegistration } from "../api";
export function RegisterForm(): JSXElement {
const modal_ctx = useContext(ModalContext);
const login_ctx = useContext(LoginContext);
const modal_ctx = useContext(ModalContext)!;
const login_ctx = useContext(LoginContext)!;
const [username, setUsername] = createSignal<string>("");
const [password, setPassword] = createSignal<string>("");
const [captcha, setCaptcha] = createSignal<string>("");
@ -19,24 +20,26 @@ export function RegisterForm(): JSXElement {
}, 1000);
}
const clearFields = (): void => {
setUsername("");
setPassword("");
setCaptcha("");
};
const success = (response: AuthResponse): void => {
setWaiting(false);
setError(false);
clearFields();
login_ctx.logIn(response.username, response.token);
modal_ctx.setOpen(false);
};
async function regPress(e: Event): Promise<void> {
e.preventDefault();
setWaiting(true);
submitRegistration(username(), password(), captcha()).then(
(token): void => {
if (token != "") {
setWaiting(false);
setError(false);
login_ctx?.setUsername(username());
login_ctx?.setToken(token);
modal_ctx?.setLoginModalOpen(false);
setUsername("");
setPassword("");
} else {
loginFailed();
}
}
);
const data = await submitRegistration(username(), password(), captcha());
if (data) success(data as AuthResponse);
else loginFailed();
}
return (
@ -72,35 +75,13 @@ export function RegisterForm(): JSXElement {
/>
<button
class={"btn btn-primary" + (error() ? " btn-error" : "")}
classList={{ "btn btn-primary": !error(), "btn btn-error": error() }}
onClick={regPress}
>
{waiting() ? "Logging in..." : "Login"}
<Show when={waiting()} fallback="Register">
Registering...
</Show>
</button>
</form>
);
}
// This function is responsible for sending the login request to the server
// and storing the token in localstorage
export async function submitRegistration(
username: string,
password: string,
captcha: string
): Promise<string> {
const response = await fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password, captcha }),
});
if (response.ok) {
const data = await response.json();
if (data.token && data.username) {
localStorage.setItem("token", data.token);
localStorage.setItem("username", data.username);
return data.token;
}
}
return "";
}

View file

@ -16,6 +16,12 @@ export interface Post extends NewPost {
votes: Votes;
}
// This is what the login and registration responses look like
export interface AuthResponse {
username: string;
token: string;
}
export async function getPosts(): Promise<Post[]> {
const res = await fetch("/api/posts");
const data = await res.json();
@ -37,3 +43,33 @@ export async function createPost(post: NewPost): Promise<void> {
body: JSON.stringify(post),
});
}
// Send the registration request to the server
export async function submitRegistration(
username: string,
password: string,
captcha: string
): Promise<AuthResponse | undefined> {
const response = await fetch("/api/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password, captcha }),
});
if (response.ok) return await response.json();
}
// Send the login request to the server
export async function submitLogin(
username: string,
password: string
): Promise<AuthResponse | undefined> {
if (username == "" || password == "") return;
const response = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }),
});
if (response.ok) return await response.json();
}