Breaking apart some large components
This commit is contained in:
parent
6b81763259
commit
9f3685caed
9 changed files with 165 additions and 145 deletions
|
@ -34,4 +34,4 @@
|
|||
"vite-plugin-qrcode": "^0.2.2",
|
||||
"vite-plugin-solid": "^2.7.2"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
|
23
client-solid/src/LoginButton.tsx
Normal file
23
client-solid/src/LoginButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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
36
client-solid/src/Menu.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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 "";
|
||||
}
|
||||
|
|
|
@ -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 "";
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
Loading…
Reference in a new issue