Breaking apart some large components
This commit is contained in:
parent
6b81763259
commit
9f3685caed
9 changed files with 165 additions and 145 deletions
|
@ -10,8 +10,8 @@ import {
|
||||||
// So far we only have one modal, but we can add more later
|
// So far we only have one modal, but we can add more later
|
||||||
// by adding more fields to this interface, or maybe an enum
|
// by adding more fields to this interface, or maybe an enum
|
||||||
interface ModalContextType {
|
interface ModalContextType {
|
||||||
loginModalOpen: Accessor<boolean>;
|
isOpen: Accessor<boolean>;
|
||||||
setLoginModalOpen: (value: boolean) => void;
|
setOpen: (value: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface LoginContextType {
|
interface LoginContextType {
|
||||||
|
@ -21,6 +21,7 @@ interface LoginContextType {
|
||||||
setUsername: (value: string) => void;
|
setUsername: (value: string) => void;
|
||||||
loggedIn: () => boolean;
|
loggedIn: () => boolean;
|
||||||
logOut: () => void;
|
logOut: () => void;
|
||||||
|
logIn: (username: string, token: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// It is unclear to me if this is the idiomatic way to do this in Solid
|
// 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() != "";
|
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 {
|
function logOut(): void {
|
||||||
localStorage.removeItem("token");
|
localStorage.removeItem("token");
|
||||||
localStorage.removeItem("username");
|
localStorage.removeItem("username");
|
||||||
|
@ -55,9 +63,9 @@ export function GlobalStateProvider(props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ModalContext.Provider value={{ loginModalOpen, setLoginModalOpen }}>
|
<ModalContext.Provider value={{ isOpen: loginModalOpen, setOpen: setLoginModalOpen }}>
|
||||||
<LoginContext.Provider
|
<LoginContext.Provider
|
||||||
value={{ token, setToken, username, setUsername, loggedIn, logOut }}
|
value={{ token, setToken, username, setUsername, loggedIn, logOut, logIn }}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
</LoginContext.Provider>
|
</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 modal_ctx = useContext(ModalContext)!;
|
||||||
|
|
||||||
const closeModal = (): void => {
|
const closeModal = (): void => {
|
||||||
modal_ctx.setLoginModalOpen(false);
|
modal_ctx.setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close the modal when the component is unmounted
|
// Close the modal when the component is unmounted
|
||||||
onCleanup(() => {
|
onCleanup(() => {
|
||||||
modal_ctx.setLoginModalOpen(false);
|
modal_ctx.setOpen(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={modal_ctx.loginModalOpen()}>
|
<Show when={modal_ctx.isOpen()}>
|
||||||
<dialog class="modal modal-open">
|
<dialog class="modal modal-open">
|
||||||
<div class="modal-box">
|
<div class="modal-box">
|
||||||
<form method="dialog">
|
<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 { A } from "@solidjs/router";
|
||||||
import { JSXElement, Show, useContext } from "solid-js";
|
import { JSXElement } from "solid-js";
|
||||||
|
|
||||||
import { LoginContext, ModalContext } from "./GlobalState";
|
import { Flake } from "./Icons";
|
||||||
import { Flake, Home, Plus, UserCircle } from "./Icons";
|
import { LoginButton } from "./LoginButton";
|
||||||
|
import { Menu } from "./Menu";
|
||||||
// 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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Navbar(): JSXElement {
|
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 (
|
return (
|
||||||
<div class="max-w navbar max-w-3xl rounded-box text-neutral-content md:my-4">
|
<div class="max-w navbar max-w-3xl rounded-box text-neutral-content md:my-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
|
@ -53,18 +18,7 @@ export function Navbar(): JSXElement {
|
||||||
<Menu />
|
<Menu />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 justify-end">
|
<div class="flex flex-1 justify-end">
|
||||||
<A
|
<LoginButton />
|
||||||
href="#"
|
|
||||||
class="btn btn-ghost text-sm capitalize"
|
|
||||||
onClick={clickHandler}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
<Show when={login_ctx.loggedIn()} fallback="Login">
|
|
||||||
{login_ctx.username()}
|
|
||||||
</Show>
|
|
||||||
}
|
|
||||||
<UserCircle />
|
|
||||||
</A>
|
|
||||||
</div>
|
</div>
|
||||||
</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 { LoginContext, ModalContext } from "../GlobalState";
|
||||||
|
import { AuthResponse, submitLogin } from "../api";
|
||||||
|
|
||||||
export function LoginForm(): JSXElement {
|
export function LoginForm(): JSXElement {
|
||||||
const modal_ctx = useContext(ModalContext);
|
const modal_ctx = useContext(ModalContext)!;
|
||||||
const login_ctx = useContext(LoginContext);
|
const login_ctx = useContext(LoginContext)!;
|
||||||
const [username, setUsername] = createSignal<string>("");
|
const [username, setUsername] = createSignal<string>("");
|
||||||
const [password, setPassword] = createSignal<string>("");
|
const [password, setPassword] = createSignal<string>("");
|
||||||
const [waiting, setWaiting] = createSignal(false);
|
const [waiting, setWaiting] = createSignal(false);
|
||||||
|
@ -18,22 +19,25 @@ export function LoginForm(): JSXElement {
|
||||||
}, 1000);
|
}, 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> {
|
async function loginPress(e: Event): Promise<void> {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setWaiting(true);
|
setWaiting(true);
|
||||||
submitLogin(username(), password()).then((token): void => {
|
const data = await submitLogin(username(), password());
|
||||||
if (token != "") {
|
if (data) success(data as AuthResponse);
|
||||||
setWaiting(false);
|
else loginFailed();
|
||||||
setError(false);
|
|
||||||
login_ctx?.setUsername(username());
|
|
||||||
login_ctx?.setToken(token);
|
|
||||||
modal_ctx?.setLoginModalOpen(false);
|
|
||||||
setUsername("");
|
|
||||||
setPassword("");
|
|
||||||
} else {
|
|
||||||
loginFailed();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -59,35 +63,13 @@ export function LoginForm(): JSXElement {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class={"btn btn-primary" + (error() ? " btn-error" : "")}
|
classList={{ "btn btn-primary": !error(), "btn btn-error": error() }}
|
||||||
onClick={loginPress}
|
onClick={loginPress}
|
||||||
>
|
>
|
||||||
{waiting() ? "Logging in..." : "Login"}
|
<Show when={waiting()} fallback="Login">
|
||||||
|
Logging in...
|
||||||
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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 { LoginContext, ModalContext } from "../GlobalState";
|
||||||
|
import { AuthResponse, submitRegistration } from "../api";
|
||||||
|
|
||||||
export function RegisterForm(): JSXElement {
|
export function RegisterForm(): JSXElement {
|
||||||
const modal_ctx = useContext(ModalContext);
|
const modal_ctx = useContext(ModalContext)!;
|
||||||
const login_ctx = useContext(LoginContext);
|
const login_ctx = useContext(LoginContext)!;
|
||||||
const [username, setUsername] = createSignal<string>("");
|
const [username, setUsername] = createSignal<string>("");
|
||||||
const [password, setPassword] = createSignal<string>("");
|
const [password, setPassword] = createSignal<string>("");
|
||||||
const [captcha, setCaptcha] = createSignal<string>("");
|
const [captcha, setCaptcha] = createSignal<string>("");
|
||||||
|
@ -19,24 +20,26 @@ export function RegisterForm(): JSXElement {
|
||||||
}, 1000);
|
}, 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> {
|
async function regPress(e: Event): Promise<void> {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setWaiting(true);
|
setWaiting(true);
|
||||||
submitRegistration(username(), password(), captcha()).then(
|
const data = await submitRegistration(username(), password(), captcha());
|
||||||
(token): void => {
|
if (data) success(data as AuthResponse);
|
||||||
if (token != "") {
|
else loginFailed();
|
||||||
setWaiting(false);
|
|
||||||
setError(false);
|
|
||||||
login_ctx?.setUsername(username());
|
|
||||||
login_ctx?.setToken(token);
|
|
||||||
modal_ctx?.setLoginModalOpen(false);
|
|
||||||
setUsername("");
|
|
||||||
setPassword("");
|
|
||||||
} else {
|
|
||||||
loginFailed();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -72,35 +75,13 @@ export function RegisterForm(): JSXElement {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class={"btn btn-primary" + (error() ? " btn-error" : "")}
|
classList={{ "btn btn-primary": !error(), "btn btn-error": error() }}
|
||||||
onClick={regPress}
|
onClick={regPress}
|
||||||
>
|
>
|
||||||
{waiting() ? "Logging in..." : "Login"}
|
<Show when={waiting()} fallback="Register">
|
||||||
|
Registering...
|
||||||
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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;
|
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[]> {
|
export async function getPosts(): Promise<Post[]> {
|
||||||
const res = await fetch("/api/posts");
|
const res = await fetch("/api/posts");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
|
@ -37,3 +43,33 @@ export async function createPost(post: NewPost): Promise<void> {
|
||||||
body: JSON.stringify(post),
|
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